Documentation
¶
Overview ¶
Package cloudsched is the cloud-mode recurring-bot scheduler: a per-org store of cron-scheduled bots and a multi-replica-safe ticker that fires each due schedule exactly once (CAS on the next-fire time, no leader election needed). The self-hosted equivalent is `iterion schedule` (host crontab); this is its cloud counterpart.
Index ¶
- Constants
- Variables
- func EnsureSchema(ctx context.Context, db *mongo.Database) error
- func NextFire(expr string, after time.Time) (time.Time, error)
- func NextFireForBot(sb ScheduledBot, after time.Time) (time.Time, error)
- func ValidateCron(expr string) error
- type GateFunc
- type LaunchFunc
- type MemoryStore
- func (s *MemoryStore) ClaimTick(_ context.Context, id string, expectedNext, newNext, firedAt time.Time) (bool, error)
- func (s *MemoryStore) Create(_ context.Context, sb ScheduledBot) error
- func (s *MemoryStore) Delete(_ context.Context, id string) error
- func (s *MemoryStore) DeleteByIntegration(_ context.Context, tenantID, integrationID string) error
- func (s *MemoryStore) Get(_ context.Context, id string) (ScheduledBot, error)
- func (s *MemoryStore) ListByIntegration(_ context.Context, tenantID, integrationID string) ([]ScheduledBot, error)
- func (s *MemoryStore) ListByTenant(_ context.Context, tenantID string) ([]ScheduledBot, error)
- func (s *MemoryStore) ListDue(_ context.Context, now time.Time, limit int) ([]ScheduledBot, error)
- func (s *MemoryStore) Update(_ context.Context, id string, patch SchedulePatch) (ScheduledBot, error)
- type MongoStore
- func (s *MongoStore) ClaimTick(ctx context.Context, id string, expectedNext, newNext, firedAt time.Time) (bool, error)
- func (s *MongoStore) Create(ctx context.Context, sb ScheduledBot) error
- func (s *MongoStore) Delete(ctx context.Context, id string) error
- func (s *MongoStore) DeleteByIntegration(ctx context.Context, tenantID, integrationID string) error
- func (s *MongoStore) Get(ctx context.Context, id string) (ScheduledBot, error)
- func (s *MongoStore) ListByIntegration(ctx context.Context, tenantID, integrationID string) ([]ScheduledBot, error)
- func (s *MongoStore) ListByTenant(ctx context.Context, tenantID string) ([]ScheduledBot, error)
- func (s *MongoStore) ListDue(ctx context.Context, now time.Time, limit int) ([]ScheduledBot, error)
- func (s *MongoStore) Update(ctx context.Context, id string, patch SchedulePatch) (ScheduledBot, error)
- type SchedulePatch
- type ScheduledBot
- type Store
- type Ticker
Constants ¶
const Collection = "scheduled_bots"
Collection is the Mongo collection name for scheduled bots.
Variables ¶
var ErrNotFound = fmt.Errorf("cloudsched: scheduled bot not found")
ErrNotFound is returned by Get/Delete for an unknown id.
Functions ¶
func EnsureSchema ¶
EnsureSchema creates the indexes. Idempotent.
func NextFireForBot ¶ added in v1.0.0
NextFireForBot computes a schedule's next-fire instant: a fixed interval for keepalive (IntervalSeconds > 0), else the cron expression. The single seam every ticker/store path uses so keepalive and cron stay consistent.
func ValidateCron ¶
ValidateCron reports whether expr is a valid 5-field standard cron.
Types ¶
type GateFunc ¶ added in v0.50.0
type GateFunc func(ctx context.Context, sb ScheduledBot) (proceed bool, guardStdout string, rec schedgate.TickRecord)
GateFunc decides, AFTER this replica won the slot's CAS, whether the launch proceeds (overlap policy + guard, pkg/schedgate). On proceed=false, rec is the skip record for the audit sink; on proceed=true rec is ignored (the ticker audits the fired outcome itself, with the launch error) and guardStdout carries the guard's stdout ("" when no guard) for injection into the launch vars. Positioned after the CAS on purpose: the slot must be consumed exactly once across replicas regardless of the gate's verdict, and only the CAS winner may run the guard (a pre-CAS gate would run it on every replica).
type LaunchFunc ¶
type LaunchFunc func(ctx context.Context, sb ScheduledBot) error
LaunchFunc fires one scheduled bot run. The cloud bootstrap wires it to the run publisher (resolve the bot, build a LaunchSpec, SubmitLaunch).
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-memory Store for tests + single-process use. ClaimTick enforces the same CAS-on-next_fire_at semantics as the Mongo store.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore returns an empty in-memory store.
func (*MemoryStore) Create ¶
func (s *MemoryStore) Create(_ context.Context, sb ScheduledBot) error
func (*MemoryStore) DeleteByIntegration ¶
func (s *MemoryStore) DeleteByIntegration(_ context.Context, tenantID, integrationID string) error
func (*MemoryStore) Get ¶
func (s *MemoryStore) Get(_ context.Context, id string) (ScheduledBot, error)
func (*MemoryStore) ListByIntegration ¶
func (s *MemoryStore) ListByIntegration(_ context.Context, tenantID, integrationID string) ([]ScheduledBot, error)
func (*MemoryStore) ListByTenant ¶ added in v0.50.0
func (s *MemoryStore) ListByTenant(_ context.Context, tenantID string) ([]ScheduledBot, error)
func (*MemoryStore) ListDue ¶
func (s *MemoryStore) ListDue(_ context.Context, now time.Time, limit int) ([]ScheduledBot, error)
func (*MemoryStore) Update ¶ added in v0.50.0
func (s *MemoryStore) Update(_ context.Context, id string, patch SchedulePatch) (ScheduledBot, error)
type MongoStore ¶
type MongoStore struct {
// contains filtered or unexported fields
}
MongoStore is the Mongo-backed Store.
func NewMongoStore ¶
func NewMongoStore(db *mongo.Database) *MongoStore
NewMongoStore builds a Mongo-backed scheduled-bot store.
func (*MongoStore) ClaimTick ¶
func (s *MongoStore) ClaimTick(ctx context.Context, id string, expectedNext, newNext, firedAt time.Time) (bool, error)
ClaimTick is the CAS: the update matches only while next_fire_at still equals expectedNext, so the first replica to advance it wins and the rest get (false, nil). exactly-once per slot, no leader.
func (*MongoStore) Create ¶
func (s *MongoStore) Create(ctx context.Context, sb ScheduledBot) error
func (*MongoStore) DeleteByIntegration ¶
func (s *MongoStore) DeleteByIntegration(ctx context.Context, tenantID, integrationID string) error
func (*MongoStore) Get ¶
func (s *MongoStore) Get(ctx context.Context, id string) (ScheduledBot, error)
func (*MongoStore) ListByIntegration ¶
func (s *MongoStore) ListByIntegration(ctx context.Context, tenantID, integrationID string) ([]ScheduledBot, error)
func (*MongoStore) ListByTenant ¶ added in v0.50.0
func (s *MongoStore) ListByTenant(ctx context.Context, tenantID string) ([]ScheduledBot, error)
func (*MongoStore) ListDue ¶
func (s *MongoStore) ListDue(ctx context.Context, now time.Time, limit int) ([]ScheduledBot, error)
func (*MongoStore) Update ¶ added in v0.50.0
func (s *MongoStore) Update(ctx context.Context, id string, patch SchedulePatch) (ScheduledBot, error)
Update applies a partial mutation. Reads the current row, mutates it via applySchedulePatch, and writes back via ReplaceOne — the atomicity that matters here is exactly-once fire (ClaimTick's CAS), not multi-writer serialisation on the mutable payload (Cron/Vars/…), so a full replace is safe and matches the semantics operators expect from a REST PATCH.
type SchedulePatch ¶ added in v0.50.0
type SchedulePatch struct {
Cron *string
IntervalSeconds *int
NextFireAt *time.Time
Vars *map[string]string
RepoURL *string
RepoRef *string
Disabled *bool
Overlap *string
MaxConcurrent *int
Guard *string
GuardTimeout *string
GuardVar *string
StaleAfter *string
UpdatedAt time.Time
}
SchedulePatch describes the mutable slice of ScheduledBot the manual CRUD endpoint exposes. Nil field = leave untouched. Cron carries the new expression when set (already validated by ValidateCron); the store recomputes NextFireAt through NextFire(cron, now).
type ScheduledBot ¶
type ScheduledBot struct {
ID string `bson:"_id" json:"id"`
TenantID string `bson:"tenant_id" json:"tenant_id"`
RepoIntegrationID string `bson:"repo_integration_id,omitempty" json:"repo_integration_id,omitempty"`
BotID string `bson:"bot_id" json:"bot_id"`
Cron string `bson:"cron" json:"cron"` // 5-field standard cron
// IntervalSeconds drives an always-on (keepalive) schedule instead of Cron:
// the ticker relaunches the bot every IntervalSeconds (sub-minute allowed,
// bounded by the ticker's own Interval). Exactly one of Cron/IntervalSeconds
// is set. Overlap=keepalive gives at-most-one-live + staleness reaping.
IntervalSeconds int `bson:"interval_seconds,omitempty" json:"interval_seconds,omitempty"`
Vars map[string]string `bson:"vars,omitempty" json:"vars,omitempty"`
RepoURL string `bson:"repo_url,omitempty" json:"repo_url,omitempty"`
RepoRef string `bson:"repo_ref,omitempty" json:"repo_ref,omitempty"`
Disabled bool `bson:"disabled,omitempty" json:"disabled,omitempty"`
// Overlap policy + pre-launch guard (pkg/schedgate). Overlap ""
// normalizes to "skip": a slot whose previous run is still live is
// consumed without launching (audited), instead of piling up runs.
Overlap string `bson:"overlap,omitempty" json:"overlap,omitempty"`
MaxConcurrent int `bson:"max_concurrent,omitempty" json:"max_concurrent,omitempty"`
Guard string `bson:"guard,omitempty" json:"guard,omitempty"`
GuardTimeout string `bson:"guard_timeout,omitempty" json:"guard_timeout,omitempty"`
GuardVar string `bson:"guard_var,omitempty" json:"guard_var,omitempty"`
// StaleAfter is the keepalive silence cutoff (Go duration); empty defaults
// to schedgate.DefaultStaleAfter. Only meaningful with Overlap=keepalive.
StaleAfter string `bson:"stale_after,omitempty" json:"stale_after,omitempty"`
// NextFireAt is the next UTC instant this schedule is due. The ticker
// CAS-advances it the moment it claims a tick, so a second replica racing
// on the same row finds next_fire_at already moved and backs off — exactly
// one fire per slot without a leader.
NextFireAt time.Time `bson:"next_fire_at" json:"next_fire_at"`
LastFireAt *time.Time `bson:"last_fire_at,omitempty" json:"last_fire_at,omitempty"`
CreatedBy string `bson:"created_by,omitempty" json:"created_by,omitempty"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
ScheduledBot is one cron-scheduled bot run. When RepoURL is set the cloud runner clones that repo before the bot starts (RepoRef pins the ref); stateful bots that persist to git (e.g. feed-watch state_commit=true) need this to have a workspace to push to. When RepoURL is empty the run executes against the runner pod's base WorkDir, matching the pre-repo behaviour.
func (ScheduledBot) Policy ¶ added in v0.50.0
func (sb ScheduledBot) Policy() schedgate.Policy
Policy projects the schedule's schedgate fields into a normalized overlap/guard policy.
type Store ¶
type Store interface {
Create(ctx context.Context, sb ScheduledBot) error
Get(ctx context.Context, id string) (ScheduledBot, error)
ListByIntegration(ctx context.Context, tenantID, integrationID string) ([]ScheduledBot, error)
// ListByTenant returns every schedule owned by tenantID (including rows
// with an empty RepoIntegrationID — the manual CRUD path). Ordered by
// CreatedAt ascending for stable rendering.
ListByTenant(ctx context.Context, tenantID string) ([]ScheduledBot, error)
// ListDue returns enabled schedules whose next_fire_at <= now (capped by
// limit, 0 = no cap), oldest-due first.
ListDue(ctx context.Context, now time.Time, limit int) ([]ScheduledBot, error)
// ClaimTick atomically advances a schedule's next_fire_at from expectedNext
// to newNext (and stamps last_fire_at = firedAt), returning true only when
// THIS caller won the CAS. A losing replica gets (false, nil). This is the
// exactly-once primitive — no leader election.
ClaimTick(ctx context.Context, id string, expectedNext, newNext, firedAt time.Time) (bool, error)
// Update applies a partial mutation to an existing schedule. Only the
// non-nil fields of patch are written; NextFireAt is recomputed from the
// new Cron when Cron is set.
Update(ctx context.Context, id string, patch SchedulePatch) (ScheduledBot, error)
Delete(ctx context.Context, id string) error
DeleteByIntegration(ctx context.Context, tenantID, integrationID string) error
}
Store persists scheduled bots. Mongo (cloud) and an in-memory impl (tests) satisfy it.
type Ticker ¶
type Ticker struct {
Store Store
Launch LaunchFunc
Interval time.Duration // default 1 minute
Logger *iterlog.Logger
// Gate, when set, runs the overlap/guard policy between the CAS win
// and the launch. Audit, when set, receives every gate decision AND
// the fired outcome. Both nil-safe (nil = fire unconditionally,
// unaudited — the pre-gate behavior).
Gate GateFunc
Audit func(rec schedgate.TickRecord)
// Now is injectable for tests; defaults to time.Now().UTC().
Now func() time.Time
}
Ticker fires due schedules. It is multi-replica-safe WITHOUT leader election: every replica may run a Ticker; the CAS in ClaimTick guarantees each slot fires exactly once (the first replica to advance next_fire_at wins; the rest see the moved value and skip).