| 1 | package registry |
| 2 | |
| 3 | import ( |
| 4 | "database/sql" |
| 5 | "errors" |
| 6 | ) |
| 7 | |
| 8 | // MaterializeTimerRegistration preserves the identity already recorded by root. |
| 9 | // The caller must retain host exclusion and check its inventory revision before |
| 10 | // this write. Replay accepts only the exact resulting row, including ownership. |
| 11 | func (r *Registry) MaterializeTimerRegistration(t Timer, expected string) (*Timer, error) { |
| 12 | if r.remote != nil { |
| 13 | out, err := r.remote.call(nil, remoteRequest{Method: "MaterializeTimerRegistration", T: t, Expected: expected}, nil) |
| 14 | return out.Timer, err |
| 15 | } |
| 16 | if !t.ClaimOwnership.Valid() || t.Registration == "" || t.Registration != t.ClaimRowID { |
| 17 | return nil, ErrClaimOwnership |
| 18 | } |
| 19 | tx, err := r.sql.Begin() |
| 20 | if err != nil { |
| 21 | return nil, err |
| 22 | } |
| 23 | defer tx.Rollback() |
| 24 | route, err := scanRoute(tx.QueryRow(`SELECT `+routeCols+` FROM routes WHERE host=?`, t.Host).Scan) |
| 25 | if err != nil { |
| 26 | if errors.Is(err, sql.ErrNoRows) { |
| 27 | return nil, ErrClaimOwnership |
| 28 | } |
| 29 | return nil, err |
| 30 | } |
| 31 | if !route.ClaimOwnership.Valid() || route.ClaimIncarnation != t.ClaimIncarnation || route.ClaimStamp != t.ClaimStamp || route.ClaimOwner != t.ClaimOwner { |
| 32 | return nil, ErrClaimOwnership |
| 33 | } |
| 34 | old, err := scanTimer(tx.QueryRow(`SELECT `+timerCols+` FROM timers WHERE host=? AND name=?`, t.Host, t.Name)) |
| 35 | switch { |
| 36 | case errors.Is(err, sql.ErrNoRows): |
| 37 | if expected != "" { |
| 38 | return nil, ErrTimerChanged |
| 39 | } |
| 40 | case err != nil: |
| 41 | return nil, err |
| 42 | default: |
| 43 | if old == t { |
| 44 | if err := tx.Commit(); err != nil { |
| 45 | return nil, err |
| 46 | } |
| 47 | return &old, nil |
| 48 | } |
| 49 | if old.Registration == t.Registration { |
| 50 | return nil, ErrTimerChanged |
| 51 | } |
| 52 | if old.Kind != t.Kind { |
| 53 | return nil, ErrTimerKind |
| 54 | } |
| 55 | if expected == "" || old.Registration != expected || old.ClaimRowID != expected || old.ClaimIncarnation != t.ClaimIncarnation || old.ClaimStamp != t.ClaimStamp || old.ClaimOwner != t.ClaimOwner { |
| 56 | return nil, ErrTimerChanged |
| 57 | } |
| 58 | 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 { |
| 59 | return nil, err |
| 60 | } |
| 61 | } |
| 62 | 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 { |
| 63 | return nil, err |
| 64 | } |
| 65 | if err := tx.Commit(); err != nil { |
| 66 | return nil, err |
| 67 | } |
| 68 | return &t, nil |
| 69 | } |
| 70 | |
| 71 | // DeleteTimerOwnedRegistration cannot remove a successor or a row with a |
| 72 | // copied registration but another installation's ownership. |
| 73 | func (r *Registry) DeleteTimerOwnedRegistration(t Timer) error { |
| 74 | if r.remote != nil { |
| 75 | _, err := r.remote.call(nil, remoteRequest{Method: "DeleteTimerOwnedRegistration", T: t}, nil) |
| 76 | return err |
| 77 | } |
| 78 | if !t.ClaimOwnership.Valid() || t.Registration == "" || t.Registration != t.ClaimRowID { |
| 79 | return ErrClaimOwnership |
| 80 | } |
| 81 | 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) |
| 82 | return timerRegistrationResult(res, err) |
| 83 | } |
| 84 | |