Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
823bef8
🚀 Implement concurrent contract save queue with deduplication
mkmccarty Jan 30, 2026
3389f72
Initial plan
Copilot Jan 30, 2026
60926e3
Fix double mutex unlock bug in saveData loop
Copilot Jan 30, 2026
11e8034
Initial plan
Copilot Jan 30, 2026
b385d84
Initial plan
Copilot Jan 30, 2026
72f3875
Initial plan
Copilot Jan 30, 2026
522cb8a
Update src/boost/boost_datastore.go
mkmccarty Jan 30, 2026
a719037
Initial plan
Copilot Jan 30, 2026
81bf5b6
Update src/boost/boost_datastore.go
mkmccarty Jan 30, 2026
a26b90d
Fix double mutex unlock bug by using continue instead of break
Copilot Jan 30, 2026
4666463
Merge branch 'mm-branch-1' into copilot/sub-pr-2131-one-more-time
mkmccarty Jan 30, 2026
2946cae
Fix race condition by moving pendingSaves deletion after processing
Copilot Jan 30, 2026
65a8416
Merge pull request #2136 from mkmccarty/copilot/sub-pr-2131-one-more-…
mkmccarty Jan 30, 2026
feaad09
Merge branch 'mm-branch-1' into copilot/sub-pr-2131-yet-again
mkmccarty Jan 30, 2026
5936bdd
Merge pull request #2135 from mkmccarty/copilot/sub-pr-2131-yet-again
mkmccarty Jan 30, 2026
b209dca
Add mutex protection for contract access during save
Copilot Jan 30, 2026
6bcbb06
Merge branch 'mm-branch-1' into copilot/sub-pr-2131-another-one
mkmccarty Jan 30, 2026
ff782e8
Merge pull request #2134 from mkmccarty/copilot/sub-pr-2131-another-one
mkmccarty Jan 30, 2026
dc5f9f5
Merge branch 'mm-branch-1' into copilot/sub-pr-2131-again
mkmccarty Jan 30, 2026
276c42e
Merge pull request #2133 from mkmccarty/copilot/sub-pr-2131-again
mkmccarty Jan 30, 2026
fe44c37
Merge branch 'mm-branch-1' into copilot/sub-pr-2131
mkmccarty Jan 30, 2026
409436c
Merge pull request #2132 from mkmccarty/copilot/sub-pr-2131
mkmccarty Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/boost/boost.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ func init() {
if err == nil {
Contracts = c
}

// Start the background save queue worker
startSaveQueueWorker()
}

func changeContractState(contract *Contract, newstate int) {
Expand Down
82 changes: 61 additions & 21 deletions src/boost/boost_datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"log"
"strings"
"sync"
"time"

"github.com/peterbourgon/diskv/v3"
Expand All @@ -21,6 +22,11 @@ var ctx = context.Background()
var ddl string
var queries *Queries

// Save queue infrastructure
var saveQueue = make(chan string, 100) // Buffer up to 100 save requests
var saveQueueMutex sync.Mutex
var pendingSaves = make(map[string]bool) // Track contracts pending save

func sqliteInit() {
db, _ := sql.Open("sqlite", "ttbb-data/ContractData.sqlite?_busy_timeout=5000")

Expand All @@ -31,6 +37,23 @@ func sqliteInit() {
queries = New(db)
}

// startSaveQueueWorker initializes the background worker that processes save requests
func startSaveQueueWorker() {
go func() {
for contractHash := range saveQueue {
// Process the actual save
processSingleContractSave(contractHash)

// Mark as no longer pending after processing completes
// This ensures that if the contract is modified during processing,
// a new save request will be queued rather than being skipped
saveQueueMutex.Lock()
delete(pendingSaves, contractHash)
saveQueueMutex.Unlock()
}
}()
}

// SaveAllData will save all contract data to disk
func SaveAllData() {
log.Print("Saving contract data")
Expand Down Expand Up @@ -69,39 +92,56 @@ func InverseTransform(pathKey *diskv.PathKey) (key string) {

func saveData(contractHash string) {
if contractHash != "" {
contract := FindContractByHash(contractHash)
if contract == nil {
return
// Queue individual contract save with deduplication
saveQueueMutex.Lock()
// If already pending, no need to add another request
if !pendingSaves[contractHash] {
pendingSaves[contractHash] = true
saveQueue <- contractHash
}

/*
if contract.State == ContractStateSignup {
if time.Since(contract.LastSaveTime) < 30*time.Second && len(contract.Boosters) < contract.CoopSize {
// Only save signup contracts every 30 seconds during signup
return
}
} else {
if time.Since(contract.LastSaveTime) < 15*time.Second {
// Only save non-signup contracts every 15 seconds
return
}
}
*/
contract.LastSaveTime = time.Now()
saveSqliteData(contract)
saveQueueMutex.Unlock()
return
}

// Save all contracts - queue each one individually
for _, c := range Contracts {
saveSqliteData(c)
c.LastSaveTime = time.Now()
contractHash := c.ContractHash
saveQueueMutex.Lock()
if !pendingSaves[contractHash] {
select {
case saveQueue <- contractHash:
// Successfully queued
pendingSaves[contractHash] = true
default:
// Queue is full, skip this one (it will be retried in the next save cycle)
log.Printf("Save queue full, skipping contract: %s", contractHash)
delete(pendingSaves, contractHash) // Remove from pending since we couldn't queue it
saveQueueMutex.Unlock()
continue
}
}
saveQueueMutex.Unlock()
}

// Legacy disk store backup
//b, _ := json.Marshal(Contracts)
//_ = dataStore.Write("EggsBackup", b)
}

// processSingleContractSave handles the actual database write for a single contract
func processSingleContractSave(contractHash string) {
contract := FindContractByHash(contractHash)
if contract == nil {
return
}

contract.mutex.Lock()
contract.LastSaveTime = time.Now()
contract.mutex.Unlock()
saveSqliteData(contract)
contract.mutex.Unlock()
}

/*
func saveEndData(c *Contract) error {
//diskmutex.Lock()
Expand Down
Loading