Artifact 692fc4426b7ba90934919ac2f6cfd362edbeb626748d16f785046f484657d851:
- File
internal/sync/ckin_lock.go
— part of check-in
[161d6ab68f]
at
2026-10-13 05:26:26
on branch main
— fix(sync): cover the sync-protocol-01 coverage rows (#394)
Check the remaining partial and uncovered sync rows, and fix the client private-gimme answer, the unversioned-file NAME encoding, the check-in lock table, the uv-hash name form and the uvgimme round limit they exposed. Tracked in #392. (user: [email protected], size: 4477) [annotate] [blame] [check-ins using]
package sync import ( "database/sql" "errors" "strconv" "strings" "time" "github.com/danmestas/go-libfossil/db" ) // DefaultCkinLockTimeout is the default duration after which a ci-lock expires. const DefaultCkinLockTimeout = 60 * time.Second // lockTimeout is the repository's lock-timeout setting in seconds, which // fossil reads with db_get_int("lock-timeout", 60) (§9.4). A setting that is // absent, not a number or not positive gives the default; a zero timeout // would expire every lock as it was taken. func lockTimeout(d *db.DB) time.Duration { var v string if d.QueryRow("SELECT value FROM config WHERE name='lock-timeout'").Scan(&v) != nil { return DefaultCkinLockTimeout } n, err := strconv.Atoi(strings.TrimSpace(v)) if err != nil || n <= 0 { return DefaultCkinLockTimeout } return time.Duration(n) * time.Second } // CkinLockFail reports that another client holds the check-in lock. type CkinLockFail struct { HeldBy string Since time.Time } // ckinLockPrefix names a check-in lock's config row, as fossil's ci-lock // does: ci-lock-HASH, holding JSON {"login":...,"clientid":...}, with the // time the lock was taken in the mtime column (§9.4). A repository served by // both fossil and this server so has one lock table. const ckinLockPrefix = "ci-lock-" func configKey(parentUUID string) string { return ckinLockPrefix + parentUUID } // releaseCkinLocks drops the check-in locks clientID holds, and any lock // whose record is not JSON, as fossil's pragma ci-unlock does (§9.4). func releaseCkinLocks(d *db.DB, clientID string) { d.Exec("DELETE FROM config WHERE name GLOB 'ci-lock-*'"+ " AND (NOT json_valid(value) OR json_extract(value, '$.clientid')=?)", clientID) } // processCkinLock handles a ci-lock pragma request, as fossil's page_xfer // does: stale locks go first; a live lock on parentUUID is kept as it is, // with no refresh, and reported unless clientID holds it; with none, a lock // is written for user and clientID, unacknowledged (§9.4). func processCkinLock(d *db.DB, parentUUID, clientID, user string, timeout time.Duration) *CkinLockFail { if parentUUID == "" { panic("sync.processCkinLock: empty parentUUID") } expireStaleLocks(d, timeout) var login, holder string var since int64 // mtime is cast: the column is declared DATETIME, which some drivers // would otherwise hand back as a time. err := d.QueryRow("SELECT coalesce(json_extract(value, '$.login'), ''),"+ " coalesce(json_extract(value, '$.clientid'), ''), CAST(mtime AS INTEGER)"+ " FROM config"+ " WHERE name=? AND json_valid(value)", configKey(parentUUID)). Scan(&login, &holder, &since) if err == nil { if holder != clientID { return &CkinLockFail{HeldBy: login, Since: time.Unix(since, 0)} } return nil } if !errors.Is(err, sql.ErrNoRows) { // A lock that cannot be read is not overwritten: the pragma has no // error reply, and taking the lock could clobber another client's. return nil } d.Exec("REPLACE INTO config(name, value, mtime)"+ " VALUES(?, json_object('login', ?, 'clientid', ?), ?)", configKey(parentUUID), user, clientID, time.Now().Unix()) return nil } // expireStaleLocks removes the ci-lock rows at least timeout old, or whose // check-in is no longer a leaf. As in fossil, only rows holding JSON are // considered; ci-unlock is what clears the others. func expireStaleLocks(d *db.DB, timeout time.Duration) { cutoff := time.Now().Unix() - int64(timeout.Seconds()) // Locks written under the old libfossil-ci-lock- key match neither this // sweep nor ci-unlock, so nothing else would ever clear them. d.Exec("DELETE FROM config WHERE name GLOB 'libfossil-ci-lock-*'") d.Exec("DELETE FROM config WHERE name GLOB 'ci-lock-*' AND json_valid(value)"+ " AND mtime <= ?", cutoff) rows, err := d.Query("SELECT name FROM config WHERE name GLOB 'ci-lock-*'" + " AND json_valid(value)") if err != nil { return } defer rows.Close() var toDelete []string for rows.Next() { var name string if rows.Scan(&name) != nil { continue } uuid := strings.TrimPrefix(name, ckinLockPrefix) var rid int64 if d.QueryRow("SELECT rid FROM blob WHERE uuid=?", uuid).Scan(&rid) != nil { toDelete = append(toDelete, name) continue } var dummy int if d.QueryRow("SELECT 1 FROM leaf WHERE rid=?", rid).Scan(&dummy) != nil { toDelete = append(toDelete, name) } } for _, name := range toDelete { d.Exec("DELETE FROM config WHERE name=?", name) } }