package registry import ( "database/sql" "errors" ) // MaterializeTimerRegistration preserves the identity already recorded by root. // The caller must retain host exclusion and check its inventory revision before // this write. Replay accepts only the exact resulting row, including ownership. func (r *Registry) MaterializeTimerRegistration(t Timer, expected string) (*Timer, error) { if r.remote != nil { out, err := r.remote.call(nil, remoteRequest{Method: "MaterializeTimerRegistration", T: t, Expected: expected}, nil) return out.Timer, err } if !t.ClaimOwnership.Valid() || t.Registration == "" || t.Registration != t.ClaimRowID { return nil, ErrClaimOwnership } tx, err := r.sql.Begin() if err != nil { return nil, err } defer tx.Rollback() route, err := scanRoute(tx.QueryRow(`SELECT `+routeCols+` FROM routes WHERE host=?`, t.Host).Scan) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrClaimOwnership } return nil, err } if !route.ClaimOwnership.Valid() || route.ClaimIncarnation != t.ClaimIncarnation || route.ClaimStamp != t.ClaimStamp || route.ClaimOwner != t.ClaimOwner { return nil, ErrClaimOwnership } old, err := scanTimer(tx.QueryRow(`SELECT `+timerCols+` FROM timers WHERE host=? AND name=?`, t.Host, t.Name)) switch { case errors.Is(err, sql.ErrNoRows): if expected != "" { return nil, ErrTimerChanged } case err != nil: return nil, err default: if old == t { if err := tx.Commit(); err != nil { return nil, err } return &old, nil } if old.Registration == t.Registration { return nil, ErrTimerChanged } if old.Kind != t.Kind { return nil, ErrTimerKind } if expected == "" || old.Registration != expected || old.ClaimRowID != expected || old.ClaimIncarnation != t.ClaimIncarnation || old.ClaimStamp != t.ClaimStamp || old.ClaimOwner != t.ClaimOwner { return nil, ErrTimerChanged } if _, err := tx.Exec(`DELETE FROM timers WHERE host=? AND name=? AND registration=? AND claim_incarnation=? AND claim_stamp=? AND claim_owner=? AND claim_row_id=?`, t.Host, t.Name, expected, t.ClaimIncarnation, t.ClaimStamp, t.ClaimOwner, expected); err != nil { return nil, err } } if _, err := tx.Exec(`INSERT INTO timers (`+timerCols+`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, t.Host, t.Name, t.Kind, t.Spec, t.Path, t.NextAt, t.LastAt, t.LastStatus, t.LastMs, t.Attempts, t.CreatedAt, t.DueAt, t.Registration, t.ClaimIncarnation, t.ClaimStamp, t.ClaimOwner, t.ClaimRowID); err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, err } return &t, nil } // DeleteTimerOwnedRegistration cannot remove a successor or a row with a // copied registration but another installation's ownership. func (r *Registry) DeleteTimerOwnedRegistration(t Timer) error { if r.remote != nil { _, err := r.remote.call(nil, remoteRequest{Method: "DeleteTimerOwnedRegistration", T: t}, nil) return err } if !t.ClaimOwnership.Valid() || t.Registration == "" || t.Registration != t.ClaimRowID { return ErrClaimOwnership } res, err := r.sql.Exec(`DELETE FROM timers WHERE host=? AND name=? AND registration=? AND claim_incarnation=? AND claim_stamp=? AND claim_owner=? AND claim_row_id=?`, t.Host, t.Name, t.Registration, t.ClaimIncarnation, t.ClaimStamp, t.ClaimOwner, t.ClaimRowID) return timerRegistrationResult(res, err) }