package registry import ( "crypto/rand" "database/sql" "encoding/hex" "errors" ) var ErrTimerChanged = errors.New("registry: timer registration changed") // UpsertTimerRegistration compares the captured registration and returns the // resulting registration in the same transaction. Empty means absent at read. func (r *Registry) UpsertTimerRegistration(t Timer, expected string) (*Timer, error) { if r.remote != nil { out, err := r.remote.call(nil, remoteRequest{Method: "UpsertTimerRegistration", T: t, Expected: expected}, nil) return out.Timer, err } tx, err := r.sql.Begin() if err != nil { return nil, err } defer tx.Rollback() old, err := scanTimer(tx.QueryRow(`SELECT `+timerCols+` FROM timers WHERE host=? AND name=?`, t.Host, t.Name)) if errors.Is(err, sql.ErrNoRows) { if expected != "" { return nil, ErrTimerChanged } } else if err != nil { return nil, err } else { if old.Kind != t.Kind { return nil, ErrTimerKind } if expected == "" || old.Registration != expected { return nil, ErrTimerChanged } } rt, err := scanRoute(tx.QueryRow(`SELECT `+routeCols+` FROM routes WHERE host=?`, t.Host).Scan) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } if err == nil && rt.ClaimOwnership.Valid() { if t.ClaimOwner != "" && (t.ClaimIncarnation != rt.ClaimIncarnation || t.ClaimStamp != rt.ClaimStamp || t.ClaimOwner != rt.ClaimOwner) { return nil, ErrClaimOwnership } t.ClaimOwnership = rt.ClaimOwnership } var id [16]byte if _, err = rand.Read(id[:]); err != nil { return nil, err } t.Registration = hex.EncodeToString(id[:]) if t.ClaimOwnership.Valid() { t.ClaimRowID = t.Registration } if err = upsertTimer(tx, t); err != nil { return nil, err } row, err := scanTimer(tx.QueryRow(`SELECT `+timerCols+` FROM timers WHERE host=? AND name=?`, t.Host, t.Name)) if err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, err } return &row, nil } func (r *Registry) SetTimerStatusRegistration(host, name, registration, status string) error { if r.remote != nil { _, err := r.remote.call(nil, remoteRequest{Method: "SetTimerStatusRegistration", Host: host, Name: name, Registration: registration, Status: status}, nil) return err } if registration == "" { return ErrTimerChanged } res, err := r.sql.Exec(`UPDATE timers SET last_status=? WHERE host=? AND name=? AND registration=?`, status, host, name, registration) return timerRegistrationResult(res, err) } func (r *Registry) FireTimerNowRegistration(host, name, registration string, now int64) error { if r.remote != nil { _, err := r.remote.call(nil, remoteRequest{Method: "FireTimerNowRegistration", Host: host, Name: name, Registration: registration, Now: now}, nil) return err } if registration == "" { return ErrTimerChanged } var id [16]byte if _, err := rand.Read(id[:]); err != nil { return err } next := hex.EncodeToString(id[:]) res, err := r.sql.Exec(`UPDATE timers SET next_at=?, due_at=?, attempts=0, registration=?, claim_row_id=CASE WHEN claim_owner='' THEN claim_row_id ELSE ? END WHERE host=? AND name=? AND registration=?`, now, now, next, next, host, name, registration) return timerRegistrationResult(res, err) } func timerRegistrationResult(res sql.Result, err error) error { if err != nil { return err } n, err := res.RowsAffected() if err == nil && n == 0 { return ErrTimerChanged } return err }