Artifact 28f094c91d24ffc54a3a1a813aa645bc69b396571cb9fd86d3b420e0ddb59e6d:
- File
internal/sync/ckin_lock.go
— part of check-in
[85c48f38eb]
at
2026-10-12 23:14:40
on branch main
— fix(sync): answer pushed config, ci-unlock and req-clusters (#305) (#359)
fossil's page_xfer refuses a pushed config card from a requester without Admin with a reset, "error not authorized to push configuration" and a break (section 9.3); drops a client's check-in locks on pragma ci-unlock (section 9.4); and answers pragma req-clusters with send_all_clusters, the cluster igots between "# sending-clusters" and "# end-of-clusters" (section 5.3). The server ignored the first two and recorded the third without answering it.
An Admin's config push is refused too, naming that this server does not apply pushed configuration, rather than dropped while the push appears to succeed. ci-unlock is honored for writers only, as ci-lock already is, and the cluster igots go to any requester, as in fossil: they name public artifacts and carry no content. (user: [email protected], size: 2791) [annotate] [blame] [check-ins using]
package sync import ( "encoding/json" "strings" "time" "github.com/danmestas/go-libfossil/db" ) // DefaultCkinLockTimeout is the default duration after which a ci-lock expires. const DefaultCkinLockTimeout = 60 * time.Second // CkinLockFail reports that another client holds the check-in lock. type CkinLockFail struct { HeldBy string Since time.Time } type ckinLockEntry struct { ClientID string `json:"clientid"` Login string `json:"login"` MTime int64 `json:"mtime"` } func configKey(parentUUID string) string { return "libfossil-ci-lock-" + 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 LIKE 'libfossil-ci-lock-%'"+ " AND (NOT json_valid(value) OR json_extract(value, '$.clientid')=?)", clientID) } // processCkinLock handles a ci-lock pragma request. // Returns non-nil CkinLockFail if another client holds the lock. func processCkinLock(d *db.DB, parentUUID, clientID, user string, timeout time.Duration) *CkinLockFail { expireStaleLocks(d, timeout) key := configKey(parentUUID) var raw string err := d.QueryRow("SELECT value FROM config WHERE name=?", key).Scan(&raw) if err == nil { var entry ckinLockEntry if json.Unmarshal([]byte(raw), &entry) == nil && entry.ClientID != clientID { return &CkinLockFail{ HeldBy: entry.Login, Since: time.Unix(entry.MTime, 0), } } } // Upsert lock. now := time.Now().Unix() entry := ckinLockEntry{ClientID: clientID, Login: user, MTime: now} val, _ := json.Marshal(entry) d.Exec(`REPLACE INTO config(name, value, mtime) VALUES(?, ?, ?)`, key, string(val), now) return nil } // expireStaleLocks removes ci-lock entries that are older than timeout // or whose parent UUID is no longer in the leaf table. func expireStaleLocks(d *db.DB, timeout time.Duration) { cutoff := time.Now().Unix() - int64(timeout.Seconds()) d.Exec("DELETE FROM config WHERE name LIKE 'libfossil-ci-lock-%' AND mtime < ?", cutoff) rows, err := d.Query("SELECT name FROM config WHERE name LIKE 'libfossil-ci-lock-%'") 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, "libfossil-ci-lock-") 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) } }