Documentation
¶
Overview ¶
Package cal is the xolu scheduling primitive: a temporal-occupancy index over bookings against calendars.
The two-layer model (H1/H3) ¶
A calendar's authoritative state is a relational booking record in SQLite (H1) — the source of truth. The structures in this package are a *derived, rebuildable bitmap index* over those bookings (H3): a per-(calendar, plane, day) occupancy raster that makes "is this span free?" and the N-way "when are all these free?" answerable by bitwise operations. The index is never authoritative; it can always be discarded and rebuilt from the SQLite records, and the invariant `index == rebuild` is the acceptance gate for every stateful operation built on top of this layer.
The conversion invariant (the load-bearing fact) ¶
Occupancy is stored at a single fine grain (5-minute quanta). Coarser views (the rollup pyramid) are *up-conversions* of the fine layer and are therefore lossy: a coarse bucket summarises many fine quanta. This asymmetry is the reason the rollup can only ever PRUNE a match (prove one impossible), never CONFIRM one (prove one exists) — confirmation always drops to the fine grain. Reconciliation between grains always moves toward the finer grain; the toward-coarser direction loses information.
This file (Stage 0) ¶
Settled design inputs pinned as constants, plus the core value types. No behaviour beyond construction lives here; the pure bit layer is in codec.go. The package deliberately knows nothing of time zones, recurrence, or DST — all timestamps are absolute UTC instants (see pkg/xolutime); calendar intentions live above this primitive.
Index ¶
- Constants
- Variables
- func BusyDayparts(rollups ...DaypartRollup) uint8
- func CandidateDaypartIndices(rollups ...DaypartRollup) []int
- func DayKey(ord CalOrdinal, plane Plane, t ot.Instant) []byte
- func DecodeKey(key []byte) (ord CalOrdinal, plane Plane, dayNanos int64, err error)
- func EncodeKey(ord CalOrdinal, plane Plane, dayNanos int64) []byte
- func IsSealed(err error) bool
- func MatchCandidateDayparts(rollups ...DaypartRollup) uint8
- func ValidEntity(h uint64) bool
- type Adapter
- func (a *Adapter) Execute(ctx context.Context, store dxp.ParticipantStore, c dxp.Claim) (dxp.Result, error)
- func (a *Adapter) PostCommit(ctx context.Context, c dxp.Claim) error
- func (a *Adapter) Release(ctx context.Context, c dxp.Claim) error
- func (a *Adapter) Reserve(ctx context.Context, tenantKey string, op dxp.OpParams, ...) (dxp.Claim, error)
- func (a *Adapter) Validate(ctx context.Context, c dxp.Claim) error
- type Booking
- type BookingConflictError
- type BookingSource
- type CalOrdinal
- type CalTransitionParams
- type Calendar
- type Capacity
- type CapacityState
- type CheckResult
- type CommitMember
- type CommitResult
- type Conflict
- type Counts
- type DayBitmap
- type DayOccupancy
- type DaypartRollup
- type IndexStore
- func (s *IndexStore) AddOccupancy(b Booking) error
- func (s *IndexStore) Check(c Calendar, span Span, mode Mode) (CheckResult, error)
- func (s *IndexStore) Close() error
- func (s *IndexStore) Match(cals []Calendar, from, to ot.Instant, duration time.Duration) (MatchResult, error)
- func (s *IndexStore) ReadOccupancy(calendarID string) (*Occupancy, error)
- func (s *IndexStore) RebuildFrom(src BookingSource) error
- func (s *IndexStore) RegisterCalendar(c Calendar)
- type Lifecycle
- func (l *Lifecycle) Cancel(calendarID, bookingID string) error
- func (l *Lifecycle) Complete(calendarID, bookingID string) error
- func (l *Lifecycle) Confirm(calendarID, bookingID string) error
- func (l *Lifecycle) Create(b Booking) (Booking, error)
- func (l *Lifecycle) Decline(calendarID, bookingID string) error
- func (l *Lifecycle) MarkMissed(calendarID, bookingID string) error
- func (l *Lifecycle) MatchCommit(when Span, members []CommitMember) (CommitResult, error)
- func (l *Lifecycle) Move(calendarID, bookingID string, to Span) (MoveResult, error)
- type Manager
- func (m *Manager) CalFor(tenantID tenant.TenantID) (*Lifecycle, error)
- func (m *Manager) Close() error
- func (m *Manager) CreateCalendar(tenantID tenant.TenantID, c Calendar) (Calendar, error)
- func (m *Manager) IndexFor(tenantID tenant.TenantID) *IndexStore
- func (m *Manager) SetOrdinalReuse(reuse bool)
- func (m *Manager) SourceFor(tenantID tenant.TenantID) *SQLiteBookingSource
- type MatchConsiders
- type MatchResult
- type MemBookingSource
- func (m *MemBookingSource) Calendars() []Calendar
- func (m *MemBookingSource) CreateCalendar(c Calendar) (Calendar, error)
- func (m *MemBookingSource) DeleteCalendar(calendarID string) error
- func (m *MemBookingSource) LiveBookings() []Booking
- func (m *MemBookingSource) LiveBookingsOn(calendarID string, plane Plane) []Booking
- func (m *MemBookingSource) PutBooking(b Booking) error
- func (m *MemBookingSource) SetStateFrom(calendarID, bookingID string, from, to State) error
- type Mode
- type MoveResult
- type Objective
- type Occupancy
- func (o *Occupancy) Add(p Plane, s Span) error
- func (o *Occupancy) Capacity(p Period) (Capacity, error)
- func (o *Occupancy) CountQuanta(p Period) (Counts, error)
- func (o *Occupancy) IsBusy(p Period) (bool, error)
- func (o *Occupancy) IsFree(p Period) (bool, error)
- func (o *Occupancy) Openings(from, to ot.Instant, duration time.Duration, obj Objective) ([]Opening, error)
- type Opening
- type Period
- type Plane
- type PlaneBookingSource
- type SQLiteBookingSource
- func (s *SQLiteBookingSource) Booking(calendarID, bookingID string) (Booking, bool)
- func (s *SQLiteBookingSource) Calendar(calendarID string) (Calendar, bool)
- func (s *SQLiteBookingSource) Calendars() []Calendar
- func (s *SQLiteBookingSource) CreateCalendar(c Calendar) (Calendar, error)
- func (s *SQLiteBookingSource) DeleteCalendar(calendarID string) error
- func (s *SQLiteBookingSource) LiveBookings() []Booking
- func (s *SQLiteBookingSource) LiveBookingsOn(calendarID string, plane Plane) []Booking
- func (s *SQLiteBookingSource) PutBooking(b Booking) error
- func (s *SQLiteBookingSource) SetStateFrom(calendarID, bookingID string, from, to State) error
- func (s *SQLiteBookingSource) TenantID() tenant.TenantID
- type SealedError
- type Sealer
- func (s *Sealer) AdvanceTo(now ot.Instant)
- func (s *Sealer) CancelSealed(calendarID, bookingID string) error
- func (s *Sealer) ConfirmSealed(calendarID, bookingID string) error
- func (s *Sealer) CreateSealed(b Booking) (Booking, error)
- func (s *Sealer) Frontier() ot.Instant
- func (s *Sealer) MoveSealed(calendarID, bookingID string, to Span) (MoveResult, error)
- type Span
- type State
- type Store
Constants ¶
const ( // QBaseSeconds is the base quantum: 5 minutes, the evidenced finest grain. // Settled for v1; re-basing to a finer grain is an open question (codec §6.1) // but never coarser. QBaseSeconds = 300 // QuantaPerDay is the number of 5-minute quanta in a UTC day: 86400/300 = 288. QuantaPerDay = 288 // WordsPerDay is the uint64 count holding QuantaPerDay bits: ceil(288/64) = 5. // Bits 288..319 (the slack in the 5th word) are unused and always zero. WordsPerDay = 5 // NsPerQuantum and NsPerDay are the nanosecond spans. NOTE: neither is a power // of two, so flooring to a day or quantum boundary is integer division // (div/mul), never a bitmask — a mask would floor to the nearest 2^k ns // (~19.5h for a "day"), which is wrong. See codec.go dayFloorNanos. NsPerQuantum int64 = QBaseSeconds * 1_000_000_000 // 300_000_000_000 NsPerDay int64 = 86_400 * 1_000_000_000 // 86_400_000_000_000 )
const ( // DaypartHours is the v1 rollup granularity: one 3-hour daypart. DaypartHours = 3 // QuantaPerDaypart is 3h of 5-minute quanta: (3*3600)/300 = 36. QuantaPerDaypart = (DaypartHours * 3600) / QBaseSeconds // 36 // DaypartsPerDay is the number of dayparts in a day: 288/36 = 8. // One rollup byte (8 bits) summarises a whole day at daypart granularity. DaypartsPerDay = QuantaPerDay / QuantaPerDaypart // 8 )
const ( EntityNil uint64 = 0x0000000000000000 // floor: no binding by design EntityMaxValid uint64 = 0xFFFFFFFFFFFFFF00 // allocator ceiling; top 256 reserved EntityTombstone uint64 = 0xFFFFFFFFFFFFFFFF // was bound, target entity deleted )
const ( // KeySize is the fixed occupancy-key width: 1+4+1+8. KeySize = 1 + 4 + 1 + 8 // 14 )
Variables ¶
var ErrBearerRequired = errors.New("cal: binding booking requires a live bearer")
ErrBearerRequired reports that a binding booking was requested without a live bearer entity handle.
var ErrCalendarExists = errors.New("cal: calendar already exists")
ErrCalendarExists reports that CreateCalendar was called with a calendar_id already present in the tenant.
var ErrIllegalTransition = errors.New("cal: illegal lifecycle transition")
ErrIllegalTransition reports that a lifecycle transition (confirm, decline, complete, cancel, mark-missed) is not permitted from the booking's current state per the A9 rules.
var ErrInvalidSpan = errors.New("cal: invalid span")
ErrInvalidSpan reports that a span carries Start >= End, or that either instant is zero.
var ErrModeNotSupported = errors.New("cal: mode not supported (only exclusive)")
ErrModeNotSupported reports that a booking was submitted with a mode outside the exclusive-only vocabulary. Introduced in v0.14.12 when ModeShared and ModeSubPrefix were removed from the type surface (see Mode godoc in pkg/cal/booking.go for the rationale).
var ErrUnknownBooking = errors.New("cal: unknown booking")
ErrUnknownBooking reports that a request references a booking_id that does not exist on the named calendar.
var ErrUnknownCalendar = errors.New("cal: unknown calendar")
ErrUnknownCalendar reports that a request references a calendar_id that does not exist in the current tenant scope.
Functions ¶
func BusyDayparts ¶
func BusyDayparts(rollups ...DaypartRollup) uint8
BusyDayparts returns the dayparts that are occupied in ANY of the given rollups (the OR aggregate unioned) — prunes availability?q=busy: a daypart with no occupancy in any calendar is definitely free at fine grain too.
func CandidateDaypartIndices ¶
func CandidateDaypartIndices(rollups ...DaypartRollup) []int
CandidateDaypartIndices returns the indices (0..7) of dayparts surviving match-pruning. Convenience over MatchCandidateDayparts.
func DayKey ¶
func DayKey(ord CalOrdinal, plane Plane, t ot.Instant) []byte
DayKey is the convenience encoder for an arbitrary instant: it floors to the instant's UTC day and encodes. This is the function write/read paths use.
func DecodeKey ¶
func DecodeKey(key []byte) (ord CalOrdinal, plane Plane, dayNanos int64, err error)
DecodeKey reverses EncodeKey. It returns the calendar ordinal, plane, and the day_unixnano (day-floored UnixNano, UTC).
func EncodeKey ¶
func EncodeKey(ord CalOrdinal, plane Plane, dayNanos int64) []byte
EncodeKey builds the fixed-width occupancy key for a (calendar, plane, day). dayNanos must already be day-floored (use DayKey for the common path, which floors for you).
func MatchCandidateDayparts ¶
func MatchCandidateDayparts(rollups ...DaypartRollup) uint8
MatchCandidateDayparts returns the dayparts that survive match-pruning across the given calendars' rollups, as a bitmask (bit d set => daypart d must be confirmed at the fine grain).
The ONLY sound prune for "is there a commonly-free fine quantum in this daypart" is: if ANY single calendar has the daypart entirely busy (no free quantum — its AnyClear bit is clear), then no common free slot can exist there, so the daypart is pruned. A daypart survives iff every calendar has at least one free quantum in it. Survival is necessary but NOT sufficient (the free quanta may not align) — hence prune-not-confirm: surviving dayparts MUST be verified against the fine layer.
func ValidEntity ¶
ValidEntity reports whether h is an assignable real handle (not a sentinel).
Types ¶
type Adapter ¶ added in v0.26.0
type Adapter struct {
// contains filtered or unexported fields
}
Adapter is cal's dxp.Participant. One Adapter per tenant assembly; safe for concurrent use.
func NewAdapter ¶ added in v0.26.0
func NewAdapter(lc *Lifecycle, src *SQLiteBookingSource, cache *dxp.MemCache) *Adapter
NewAdapter wires lc (for spanConflicts and calendar lookup) and src (for the transactional H1 write Execute needs) into cache, and returns an Adapter ready to register with a dxp coordinator under the primitive key "cal". lc and src must be the same tenant assembly's Lifecycle/SQLiteBookingSource pair — mirroring cal.Manager.assemble's own construction, not a separate wiring path.
func (*Adapter) Execute ¶ added in v0.26.0
func (a *Adapter) Execute(ctx context.Context, store dxp.ParticipantStore, c dxp.Claim) (dxp.Result, error)
Execute applies tp's booking via putBookingInTx against the coordinator-supplied tx (proposal §11: one SQL transaction for every participant; the coordinator opens and commits tx, never Execute). It does NOT touch the Pebble occupancy index (H3) — that plane is advisory (chronicle-substrate.md §4b/dxp-composed- commitment.md: "no guard ever consults it"), and updating it here, before the coordinator's own commit is final, would be actively wrong, not just premature: in the collapsed path Execute runs before the barrier T-99's own fix introduced, so the write it just made is not yet durable; in the phased path a sibling participant can still fail and tear the instance after this one's own commit succeeds. PostCommit (below) is where H3 actually gets updated, and only once the coordinator itself knows the instance is genuinely, irreversibly committed. pending is deliberately NOT cleared here any more (T-108) — PostCommit needs tp.BookingID to re-read the final booking from H1, and Release cleans it up on every path that isn't a genuine full-instance commit.
func (*Adapter) PostCommit ¶ added in v0.26.0
PostCommit brings H3 (the Pebble occupancy index) up to date for a booking the coordinator has now confirmed genuinely, durably committed — the mechanism T-83 named as missing, built directly against dxp.Participant's own new doc, not improvised.
Re-reads the booking from H1 rather than trusting Execute's own stashed tp: by the time PostCommit runs, H1 is the one thing guaranteed final, and re-reading is the same discipline Execute's own doc already established for why it must NOT read through a.src while a transaction is open — here no transaction is open (the coordinator's own commit and its separate terminal-marking transaction have both already closed by construction, since PostCommit only ever fires after that), so the read is safe, and reading fresh rather than trusting a snapshot from before commit is the more honest source of truth regardless.
addToPlane is documented safe to call more than once (an OR over a bitmap cannot corrupt shared bits by re-adding the same booking), so no additional idempotency guard is needed here beyond that.
func (*Adapter) Release ¶ added in v0.26.0
Release drops txn's stashed params, if any. Idempotent and unconditional, matching bal/fsm exactly. The cache entry itself is removed by the coordinator's ReleaseTxn, not here.
func (*Adapter) Reserve ¶ added in v0.26.0
func (a *Adapter) Reserve(ctx context.Context, tenantKey string, op dxp.OpParams, txn, participantID string, deadline int64, w dxp.Weight) (dxp.Claim, error)
Reserve admission-checks tp against BOTH the ordinary booking path (spanConflicts against H1 — the cross-path guarantee: a live ordinary booking must refuse a new dxp reservation attempting the same span) and every live dxp claim on each day-bucket the span touches (the mixed-weight admission rule: a live pessimistic claim refuses any new reservation of either weight; a live optimistic claim only refuses a new pessimistic one — matching fsm's own Reserve exactly). On consent it Holds one claim per day-bucket, all under txn, and stashes tp for Execute/Validate. The whole evaluate-then-hold sequence runs under one tenant.Lock/Unlock critical section (proposal §4), matching bal/fsm.
func (*Adapter) Validate ¶ added in v0.26.0
Validate re-runs Reserve's FULL admission check (both the ordinary and dxp-path halves, across every day-bucket the original span touches, not just the single bucket c.Resource happens to name) — c alone cannot carry enough information for a partial re-check the way bal's single-resource Validate can, so this reconstructs the full reservation from pending[c.Txn] first. Optimistic claims are invisible to guard arithmetic everywhere (bal's own §7 doc, generalised) and pass unconditionally, matching bal/fsm.
type Booking ¶
type Booking struct {
BookingID string
CalendarID string
State State
Span Span // [Start, End) UTC
Mode Mode // exclusive (the only valid value; see Mode godoc)
Bearer uint64 // entity handle (A10); required live for binding
BufferAfter ot.Instant // optional: end+buffer; zero Instant = no buffer
CreatedAt ot.Instant
UpdatedAt ot.Instant
DetailRef string // optional ref into the meta/detail document
}
Booking is the authoritative booking record (H1). Times are absolute UTC instants (xolutime); the originating wall-clock intention is NOT stored here — it is the caller's, per R-T1.
type BookingConflictError ¶ added in v0.26.0
BookingConflictError reports a dxp reservation refused because the requested span conflicts with either a live ordinary booking or a live competing dxp reservation.
func (*BookingConflictError) Error ¶ added in v0.26.0
func (e *BookingConflictError) Error() string
type BookingSource ¶
type BookingSource interface {
// Calendars returns every calendar definition.
Calendars() []Calendar
// LiveBookings returns every booking in a state that occupies a plane
// (proposed/binding/honoured); terminal-state bookings are excluded.
LiveBookings() []Booking
}
BookingSource provides the live bookings and calendars the index derives from. Stage 3 implements it in memory (MemBookingSource); Stage 3's SQLite wiring will provide a Pebble/SQLite-backed implementation behind the same interface, so Rebuild and the invariant test are storage-agnostic.
type CalOrdinal ¶
type CalOrdinal uint32
CalOrdinal is the per-tenant dense calendar identifier (codec §3.2), assigned at cal/def time and recorded in the SQLite calendar record. uint32 (the one width the codec widens over ts's uint16 timeline id).
type CalTransitionParams ¶ added in v0.26.0
type CalTransitionParams struct {
CalendarID string `json:"calendar"`
BookingID string `json:"booking_id,omitempty"`
Span Span `json:"span"`
Mode Mode `json:"mode,omitempty"`
Bearer uint64 `json:"bearer,omitempty"`
State State `json:"state,omitempty"` // target state; zero value defers to the calendar's DefaultState
}
CalTransitionParams is cal's dxp.OpParams (T-54's typed-per-primitive decision): places a booking on CalendarID at Span, admission-checked and held under one dxp reservation, executed as a real H1 write on commit. State defaults to the calendar's own DefaultState when unset (matching MatchCommit's own convention).
v1 scope, deliberate, matching bal.TransferParams' own precedent of naming what's deferred rather than silently narrowing: this reserves CREATION of a new booking only (the proposed/binding placement a hotel-style dxp def actually needs, per dxp-composed-commitment.md's own worked example). Confirming an already-proposed booking (the ordinary CalConfirm path) is not part of this v1 — nothing here forecloses adding it as a second OpParams shape later.
func (CalTransitionParams) Primitive ¶ added in v0.26.0
func (CalTransitionParams) Primitive() string
Primitive satisfies dxp.OpParams.
type Calendar ¶
type Calendar struct {
CalendarID string
Ordinal CalOrdinal // dense per-tenant key-space coordinate (codec §3.2)
EntityRef uint64 // the one entity this calendar tracks (§1)
DefaultState State // proposed | binding (§1)
MatchPolicy MatchConsiders
}
Calendar is the calendar definition record (the policy fields the substrate would otherwise have to guess, cal-rest-api.md §1, plus GATE-1's match policy).
Cal implements exclusive-only occupancy (see Mode godoc). The Capacity field that existed prior to v0.14.13 has been removed: it was descriptive metadata only, the occupancy engine never honoured N > 1, and the design review with Google Calendar's model confirmed exclusive-only as cal's target. Callers that need "how many humans fit in this room" metadata should carry it on a separate entity record, not on the calendar itself.
type Capacity ¶
type Capacity struct {
State CapacityState `json:"state"`
Capacity int `json:"capacity"` // 100 − binding%
Counts Counts `json:"counts"`
}
Capacity is the full q=capacity result: the ternary state, the scalar capacity = 100 − confirmed% (binding share; proposals ignored), and the raw counts.
type CapacityState ¶
type CapacityState string
CapacityState is the q=capacity ternary (§3b).
const ( StateFree CapacityState = "free" // no commitments at all StateIdk CapacityState = "idk" // proposed present, no binding (uncertain) StateBusy CapacityState = "busy" // binding present )
type CheckResult ¶
type CheckResult struct {
Feasible bool
NearestOpenings []Span // when infeasible: where "yes" lives nearby
}
CheckResult is the outcome of a feasibility check: feasible, or the conflicts and nearest openings.
type CommitMember ¶
type CommitMember struct {
CalendarID string
BookingID string
Mode Mode
Bearer uint64 // required live for a binding placement
}
CommitMember is one calendar's part of a cross-calendar placement.
type CommitResult ¶
type CommitResult struct {
Committed bool
Placed []string // booking ids placed (only when Committed)
Blocking []string // calendars that blocked (only when not Committed)
Conflicts []Conflict
}
CommitResult reports the outcome. On success Committed is true and Placed lists the booking ids. On conflict Committed is false, nothing was written, and Blocking names the calendars whose occupancy prevented placement.
type Conflict ¶
type Conflict struct {
With string // booking id clashed with
Over Span // the overlapping region
Reason string // fixed enum (exclusive-vs-exclusive overlap, etc.)
}
Conflict is one entry of the universal conflict report (§6), shared by create, check, and move.
type Counts ¶
Counts holds quantum tallies over a period. Binding = quanta with a binding commitment; Proposed = quanta proposed-but-not-binding (proposed set, binding clear); Free = quanta with no commitment of any kind. The three partition the period's quanta: Binding + Proposed + Free == total quanta in the period.
type DayBitmap ¶
type DayBitmap [WordsPerDay]uint64
DayBitmap is one day's occupancy raster on one plane: 288 bits in 5 uint64 words. Bit q (0..287) set means quantum q of that day is occupied; q = seconds_into_day / 300. Bits 288..319 are unused and must stay zero.
func AndFree ¶
AndFree returns the day's worth of commonly-FREE quanta across all the given busy-bitmaps: a bit is set in the result iff that quantum is free in EVERY input. This is the N-way match kernel (F16): "when are all these calendars simultaneously free?" Implemented as the AND of each input's free-mask.
With no inputs the result is the all-free day (every valid quantum set), which is the correct identity for an empty intersection.
func (DayBitmap) And ¶
And returns the bitwise intersection of two days (occupied in BOTH). This is the per-day kernel of the N-way match: a quantum free in the result of ANDing "busy" maps is... not what we want — match operates on FREE maps. See AndFree below, which is the matching primitive. And is the raw busy-AND, exposed for completeness and testing.
func (DayBitmap) IsZero ¶
IsZero reports whether the day has no occupancy (the sparse-store "no value" equivalent — an all-zero day need not be stored at all).
func (DayBitmap) Or ¶
Or returns the bitwise union (occupied in EITHER) — e.g. combining binding and proposed planes when a caller asks for both (the GATE-1 binding+proposed case; the bit layer provides the operation, the policy decision lives above).
type DayOccupancy ¶
DayOccupancy is one day's contribution from a span: the day key's floored UnixNano and the bits that day should have set.
func SpanDays ¶
func SpanDays(s Span) ([]DayOccupancy, error)
SpanDays expands a half-open [Start, End) span into per-day occupancy. Returns an error for an invalid span. A span ending exactly on a midnight contributes nothing to that midnight's day (half-open: the end quantum is exclusive).
type DaypartRollup ¶
type DaypartRollup struct {
Or uint8 // daypart has ≥1 occupied quantum
AnyClear uint8 // daypart has ≥1 free quantum
}
DaypartRollup is one day's rollup: two 8-bit masks, one bit per 3-hour daypart. Or bit d set => some quantum in daypart d is occupied. AnyClear bit d set => some quantum in daypart d is free.
func RollupDay ¶
func RollupDay(b DayBitmap) DaypartRollup
RollupDay computes both daypart aggregates of a fine day bitmap.
type IndexStore ¶
type IndexStore struct {
// Fault-injection hooks (T-31, v0.14.14). Non-nil hooks are called
// at the top of the corresponding mutation and, if they return a
// non-nil error, the mutation aborts with that error before any
// bitmap change is persisted. Nil hooks are the normal path.
//
// Intended for tests exercising the SQL/index disagreement recovery
// path — see cal_fault_injection_test.go. Not part of the public
// API contract; the fields are exported only so tests in the same
// package can set them without a reflection dance.
AddToPlaneFaultHook func(b Booking) error
RemoveFromPlaneFaultHook func(b Booking, plane Plane) error
// contains filtered or unexported fields
}
IndexStore is the Pebble-backed occupancy index (the derived bitmap, H3). It is NOT authoritative: it is a pure function of the live booking records (H1), and can be discarded and rebuilt at any time (Rebuild). The booking records themselves live in the tenant's primary store (SQLite); this package's Stage 3 works against an in-memory BookingSource for them, so the index logic and the `index == rebuild` invariant can be exercised before the SQLite wiring lands.
Storage layout mirrors ts: a Pebble db under <calDir>/db. Keys are the 14-byte occupancy keys (codec §3.2); values are 40-byte day bitmaps. Only days with occupancy have a value (sparse). The store opens at the directory returned by storelayout.TenantCalDir(base, tenantID).
func OpenIndexStore ¶
func OpenIndexStore(dir string) (*IndexStore, error)
OpenIndexStore opens (creating if needed) the cal occupancy index at dir. dir is typically storelayout.TenantCalDir(base, tenantID); the Pebble database lives in dir/db, matching the ts convention.
func (*IndexStore) AddOccupancy ¶
func (s *IndexStore) AddOccupancy(b Booking) error
AddOccupancy is the incremental write path: register a newly-created or state-changed booking's occupancy into the index. For a create or a confirm (which adds to a plane) this is an OR. For a state change that MOVES a booking between planes (proposed->binding on confirm) or REMOVES it (cancel), the caller must remove the old contribution too; the always-correct path for any removal is RebuildFrom, since overlapping bookings make incremental clear unsafe. Stage 5 (lifecycle) refines this; Stage 3 uses AddOccupancy for create and RebuildFrom as the removal/correctness path.
func (*IndexStore) Check ¶
func (s *IndexStore) Check(c Calendar, span Span, mode Mode) (CheckResult, error)
Check reports whether a booking of the given span/mode would be feasible on a calendar right now, writing nothing. Same occupancy engine as a real create, so check and create cannot drift. For an exclusive booking, feasible means the span is entirely free of any commitment (binding or proposed) on the calendar.
When infeasible, NearestOpenings offers free spans of the same duration within a search window around the requested time (the "here's where yes lives" hint).
func (*IndexStore) Close ¶
func (s *IndexStore) Close() error
Close closes the underlying Pebble database.
func (*IndexStore) Match ¶
func (s *IndexStore) Match(cals []Calendar, from, to ot.Instant, duration time.Duration) (MatchResult, error)
Match returns spans within [from,to) where every named calendar is simultaneously free for at least `duration`, honouring each calendar's match_considers policy. cals must be resolved Calendar records (the caller looks them up; Match needs their ordinals and policies).
The day-level rollup prunes days that cannot contain a coincidence before the fine AND runs (prune-not-confirm: a pruned day definitely has none; a surviving day is confirmed at fine grain). When the result is empty, Blocking names the calendars responsible.
func (*IndexStore) ReadOccupancy ¶
func (s *IndexStore) ReadOccupancy(calendarID string) (*Occupancy, error)
ReadOccupancy loads the index into an in-memory Occupancy window for a single calendar, so the Stage-2 availability reads run against persisted data. It scans the calendar's occupancy keys on both planes.
func (*IndexStore) RebuildFrom ¶
func (s *IndexStore) RebuildFrom(src BookingSource) error
RebuildFrom discards the entire occupancy keyspace and reconstructs it from the source's live bookings. This is both the recovery path and the test oracle for the index == rebuild invariant.
func (*IndexStore) RegisterCalendar ¶
func (s *IndexStore) RegisterCalendar(c Calendar)
RegisterCalendar records a calendar's ordinal in the in-memory map.
type Lifecycle ¶
type Lifecycle struct {
// contains filtered or unexported fields
}
Lifecycle wires booking-state transitions to both the authoritative record (via the source) and the derived index (incrementally). The source is the H1 truth; the index is kept in step on each transition.
func NewLifecycle ¶
func NewLifecycle(src Store, index *IndexStore) *Lifecycle
NewLifecycle binds a source and an index.
func (*Lifecycle) Create ¶
Create inserts a new booking in the calendar's default state and adds its occupancy to the index. Returns the created booking.
func (*Lifecycle) MarkMissed ¶
MarkMissed: binding -> missed (the sweeper-written non-occurrence, §7). Exposed for the reconciliation sweeper; not a user endpoint.
func (*Lifecycle) MatchCommit ¶
func (l *Lifecycle) MatchCommit(when Span, members []CommitMember) (CommitResult, error)
MatchCommit places one booking per calendar at the given span, atomically. Each member's booking lands in its calendar's default state (proposed or binding). All N land or none do.
func (*Lifecycle) Move ¶
func (l *Lifecycle) Move(calendarID, bookingID string, to Span) (MoveResult, error)
Move atomically reschedules a booking to a new span. It re-runs placement at the destination: if the destination is infeasible the booking is left exactly where it was and Moved is false with the conflict report. On success the booking occupies the new span and the prior span is gone from the index.
move never cascades to dependents; if any exist they are reported as stranded. (Dependency tracking is not in v1's record, so StrandedDependents is always empty here; the field exists so the contract is visible and stable.)
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager is the per-tenant assembly point for cal: it binds each tenant's SQLite booking source (over the tenant's primary store DB) to its Pebble occupancy index (at the storelayout cal path) and a lifecycle engine over the pair. The REST handlers sit on the Manager: one CalFor(tenant) call yields a ready lifecycle.
Assemblies are built lazily and cached per tenant (CalFor is idempotent), the same model the server uses for per-tenant sulpher job managers. The Manager does not own the *sql.DB (the caller's store does); it owns the index stores it opens, which Close releases.
func NewManager ¶
NewManager binds a base directory (under which per-tenant cal index stores are opened at storelayout.TenantCalDir) and the tenant primary-store *sql.DB. The ordinal-reuse policy defaults to retire (false).
func (*Manager) CalFor ¶
CalFor returns the lifecycle engine for a tenant, assembling it on first use. Idempotent: repeated calls for the same tenant return the same lifecycle.
func (*Manager) Close ¶
Close releases every assembled tenant's index store. The *sql.DB is owned by the caller's store and is NOT closed here.
func (*Manager) CreateCalendar ¶
CreateCalendar creates a calendar in the given tenant's persistence layer AND registers it with the in-memory index. This is the transactional operation callers should almost always use rather than composing SourceFor(t).CreateCalendar(c) with IndexFor(t).RegisterCalendar(c) themselves.
Without the index registration step, subsequent Lifecycle.Create calls against the calendar fail with ErrUnknownCalendar from IndexStore.ordinalFor — the index rebuild only runs at first assemble(), so any calendar added after that point is invisible to the ordinal map until explicitly registered. This facade eliminates that footgun.
Rollback semantics: the SQL insert runs first. If it fails, no index change occurs. If it succeeds, the index register cannot fail (it is a pure in-memory map update). Consequently the observable state after this method returns is either "both persisted and indexed" (nil error) or "neither" (non-nil error, with sentinel wrapping via the source layer).
Concurrency: the index register runs under IndexStore's own mutex. The SQL insert runs under SQLite's transaction semantics. Callers concurrently creating the same calendar will see one succeed and the others fail with ErrCalendarExists (wrapped by the source layer).
Introduced in v0.14.10.
func (*Manager) IndexFor ¶
func (m *Manager) IndexFor(tenantID tenant.TenantID) *IndexStore
IndexFor returns the tenant's occupancy index store (assembling if needed).
func (*Manager) SetOrdinalReuse ¶
SetOrdinalReuse selects the OrdinalReuse policy for sources assembled after the call (GATE-3 #2). Must be set before the first CalFor for a tenant to take effect for that tenant.
type MatchConsiders ¶
type MatchConsiders string
MatchConsiders is the GATE-1 per-calendar match policy.
const ( ConsiderBinding MatchConsiders = "binding" // optimistic: proposals don't block ConsiderBindingProposed MatchConsiders = "binding+proposed" // pessimistic: proposals block )
type MatchResult ¶
type MatchResult struct {
Matches []Span
Checked []string
Blocking []string // calendars with zero free coincidence (only set when Matches empty)
}
MatchResult is the outcome of a match: coincident free spans, or the calendars that blocked any coincidence.
type MemBookingSource ¶
type MemBookingSource struct {
// contains filtered or unexported fields
}
MemBookingSource is an in-memory implementation of BookingSource: the authoritative booking + calendar records (H1) held in maps. Stage 3 uses it to exercise the index write path and the index == rebuild invariant before the SQLite wiring lands. The SQLite-backed source will implement the same BookingSource interface, so Rebuild and the invariant test are unchanged.
It also owns the dense per-tenant cal_ordinal counter (GATE-3 #5): a uint32 allocated ascending from 1. OrdinalReuse policy governs whether a deleted calendar's ordinal returns to the pool; the default is retire (counter only moves up).
func NewMemBookingSource ¶
func NewMemBookingSource(reuse bool) *MemBookingSource
NewMemBookingSource returns an empty source. reuse selects the OrdinalReuse policy (false = retire, the safe default; true = reuse retired ordinals).
func (*MemBookingSource) Calendars ¶
func (m *MemBookingSource) Calendars() []Calendar
Calendars implements BookingSource.
func (*MemBookingSource) CreateCalendar ¶
func (m *MemBookingSource) CreateCalendar(c Calendar) (Calendar, error)
CreateCalendar registers a calendar, allocating its dense ordinal. The caller supplies policy fields; Ordinal is assigned here and returned in the stored record.
func (*MemBookingSource) DeleteCalendar ¶
func (m *MemBookingSource) DeleteCalendar(calendarID string) error
DeleteCalendar removes a calendar and (under reuse policy) returns its ordinal to the pool. Its bookings must already be gone.
func (*MemBookingSource) LiveBookings ¶
func (m *MemBookingSource) LiveBookings() []Booking
LiveBookings implements BookingSource: only plane-occupying states.
func (*MemBookingSource) LiveBookingsOn ¶
func (m *MemBookingSource) LiveBookingsOn(calendarID string, plane Plane) []Booking
LiveBookingsOn implements PlaneBookingSource: live bookings for one calendar that occupy the given plane.
func (*MemBookingSource) PutBooking ¶
func (m *MemBookingSource) PutBooking(b Booking) error
PutBooking inserts or updates a booking record (the authoritative H1 write).
func (*MemBookingSource) SetStateFrom ¶
func (m *MemBookingSource) SetStateFrom(calendarID, bookingID string, from, to State) error
SetStateFrom transitions a booking to a new state (the A9 lifecycle write) if and only if it is still in the expected from-state — the compare half of the T-34 compare-and-swap, evaluated under the source lock so concurrent racers serialise here.
type Mode ¶
type Mode string
Mode is the exclusivity vocabulary. Cal implements exclusive-only resource semantics matching Google Calendar's room model: a booking takes the whole calendar for its span, and any overlapping booking is a conflict.
The wire vocabulary previously included ModeShared ("consumes one unit of capacity") and ModeSubPrefix ("sub:<child_id>") as reserved future extensions for pooled and sub-resource semantics. Both were removed in v0.14.12 after a design review compared cal against Google Calendar's actual model (rooms are boolean; capacity is descriptive metadata for filtering, not a booking-concurrency limit) and confirmed cal's target is the same shape. Implementing pooled resources properly would require a counter-based bitmap encoding with ~8x storage growth and no user-facing feature that Google Calendar itself provides; the vocabulary was removed rather than left as accepted-but-inert.
const (
ModeExclusive Mode = "exclusive" // whole resource, the only valid value
)
type MoveResult ¶
type MoveResult struct {
Moved bool
Conflicts []Conflict
StrandedDependents []string // dependents left behind (reported, never cascaded)
}
MoveResult reports the outcome of a move. On conflict the booking is untouched and Moved is false.
type Occupancy ¶
type Occupancy struct {
// contains filtered or unexported fields
}
Occupancy is an in-memory occupancy window for one calendar: sparse per-day bitmaps on each plane, keyed by day-floored UnixNano. Only days with occupancy are present (the sparse-store model). This is the Stage-2 stand-in for the persisted index; Stage 3 replaces the maps with Pebble-backed storage behind the same read methods.
func (*Occupancy) Add ¶
Add ORs a span's occupancy into the given plane. Invalid spans are rejected.
func (*Occupancy) Capacity ¶
Capacity computes q=capacity over the period.
- state: binding present => busy; else proposed present => idk; else free.
- capacity: 100 − (binding quanta / total quanta · 100), rounded; proposals do not reduce it.
func (*Occupancy) CountQuanta ¶
CountQuanta tallies binding/proposed/free quanta over the period. A quantum is Binding if its binding bit is set; else Proposed if its proposed bit is set; else Free. (Binding dominates: a quantum both binding and proposed counts as Binding — it is genuinely taken.)
func (*Occupancy) IsBusy ¶
IsBusy reports q=busy: is there ONE OR MORE commitment of any kind? The exact complement of IsFree.
func (*Occupancy) IsFree ¶
IsFree reports q=free: are there ZERO commitments of any kind in the period?
func (*Occupancy) Openings ¶
func (o *Occupancy) Openings(from, to ot.Instant, duration time.Duration, obj Objective) ([]Opening, error)
Openings returns spans wide enough for duration within [from,to), ordered per objective. Cal implements exclusive-only occupancy: every booking takes the whole calendar for its span (see Mode godoc in booking.go). Buffer-after aftermath holds are honoured; capacity sub-units are not applicable in the exclusive-only model.
type Opening ¶
type Opening struct {
Start ot.Instant
End ot.Instant
Margin time.Duration // length of the containing free run
}
Opening is a free span wide enough for the requested duration.
type Period ¶
Period is the resolved time range of an availability query. The REST layer parses human/ISO period strings ("2027/month/05") into a Period; the bit layer works in absolute instants only.
type Plane ¶
type Plane uint8
Plane selects the binding (confirmed) or proposed (tentative) occupancy plane. The two planes are stored under distinct keys; their semantics in matching are governed by GATE-1 (codec §6.5) and are not decided in the bit layer.
type PlaneBookingSource ¶
type PlaneBookingSource interface {
BookingSource
// LiveBookingsOn returns live bookings for calendarID whose state occupies
// the given plane.
LiveBookingsOn(calendarID string, plane Plane) []Booking
}
PlaneBookingSource extends BookingSource with the scoped query the incremental maintenance needs: the live bookings on one calendar that occupy a given plane. The in-memory and (future) SQLite sources both implement it.
type SQLiteBookingSource ¶
type SQLiteBookingSource struct {
// contains filtered or unexported fields
}
SQLiteBookingSource is the persistent, authoritative booking record (H1), backed by the S11 cal_* tables in the tenant's primary store. It implements the same Store interface as MemBookingSource, so the lifecycle, commit, and seal logic — and every test — are unchanged behind it.
TIME DISCIPLINE (xolutime invariant, docs/TIME_HANDLING.md):
- Every persisted timestamp column is an absolute UTC instant stored as int64 UnixNano. The boundary is exactly instantToNanos / nanosToInstant below — instant.UnixNano() (always UTC, since ot.Instant is UTC-internal) on write, ot.FromUnixNano(ns) on read. No time.Time ever crosses this boundary, and this file never calls time.Now(): "now" for created/updated is minted upstream via ot.Now() (in Lifecycle.Create) and merely serialised here, so storage is a pure serialiser with no clock access.
Tenancy follows xolu config (GATE-3 #1): the tenant_id column discriminates in shared-file mode and is constant 0 in per-file mode, identical to every other v2 table. The caller supplies the tenant id; this source does not decide it.
func NewSQLiteBookingSource ¶
NewSQLiteBookingSource binds a source to an open *sql.DB (the tenant's primary store, with the S11 schema already migrated) and a tenant id. reuse selects the OrdinalReuse policy (false = retire, the safe default).
func (*SQLiteBookingSource) Booking ¶
func (s *SQLiteBookingSource) Booking(calendarID, bookingID string) (Booking, bool)
Booking is the exported booking accessor: returns the persisted record for a (calendar, booking) pair. Used by callers outside the package (and tests) that need to read a booking back from the authoritative store.
func (*SQLiteBookingSource) Calendar ¶
func (s *SQLiteBookingSource) Calendar(calendarID string) (Calendar, bool)
Calendar is the exported calendar accessor: returns the persisted calendar record for an id. Used by callers outside the package (and tests).
func (*SQLiteBookingSource) Calendars ¶
func (s *SQLiteBookingSource) Calendars() []Calendar
Calendars implements BookingSource.
func (*SQLiteBookingSource) CreateCalendar ¶
func (s *SQLiteBookingSource) CreateCalendar(c Calendar) (Calendar, error)
CreateCalendar inserts a calendar, allocating its dense ordinal via the cal_ord_seq monotonic allocator (the SQLite analogue of the in-memory counter; GATE-3 #5). Defaults mirror MemBookingSource.
func (*SQLiteBookingSource) DeleteCalendar ¶
func (s *SQLiteBookingSource) DeleteCalendar(calendarID string) error
DeleteCalendar removes a calendar. Its bookings must already be gone.
func (*SQLiteBookingSource) LiveBookings ¶
func (s *SQLiteBookingSource) LiveBookings() []Booking
LiveBookings implements BookingSource: every booking in a plane-occupying state. The state filter is pushed into SQL using the live-state set.
func (*SQLiteBookingSource) LiveBookingsOn ¶
func (s *SQLiteBookingSource) LiveBookingsOn(calendarID string, plane Plane) []Booking
LiveBookingsOn implements PlaneBookingSource: live bookings for one calendar on one plane. The plane maps to its state set; the (calendar_id, state) index serves this — the hot path for lifecycle/move/commit.
func (*SQLiteBookingSource) PutBooking ¶
func (s *SQLiteBookingSource) PutBooking(b Booking) error
PutBooking inserts or replaces a booking record.
func (*SQLiteBookingSource) SetStateFrom ¶
func (s *SQLiteBookingSource) SetStateFrom(calendarID, bookingID string, from, to State) error
SetStateFrom transitions a booking's state if and only if it is still in the expected from-state. The guarded UPDATE is the compare half of the T-34 compare-and-swap: under concurrent racers, SQLite serialises the writes and exactly one matches the WHERE state=? clause; the rest see zero rows affected and fail with ErrIllegalTransition.
func (*SQLiteBookingSource) TenantID ¶ added in v0.26.0
func (s *SQLiteBookingSource) TenantID() tenant.TenantID
TenantID returns the source's bound tenant identity — the dxp cal adapter uses this to validate the coordinator-supplied tenant key against the store it actually holds, matching bal.Store.TenantID's own precedent exactly.
type SealedError ¶
SealedError is returned when a mutation targets a sealed (immutable past) day.
func (*SealedError) Error ¶
func (e *SealedError) Error() string
type Sealer ¶
type Sealer struct {
// contains filtered or unexported fields
}
Sealer manages the now-crossing seal frontier for an IndexStore + Lifecycle. It serialises frontier advances against mutations via a single mutex, so the seal and the confirm cross-plane move can never destructively interleave.
func NewSealer ¶
NewSealer binds a sealer to a lifecycle. The frontier starts at zero (nothing sealed).
func (*Sealer) AdvanceTo ¶
AdvanceTo advances the seal frontier to `now`, sealing every day whose window has fully passed. It is monotone: a request to move the frontier backward is a no-op. Advancing takes the mutex, so it cannot interleave with a guarded mutation — the seal observes only fully-applied index states.
Sealing here is a logical freeze: because the sealed past is simply never mutated again (guarded below), no per-day rewrite is required at seal time. The frontier IS the seal. (A physical freeze — copying sealed days to a cold store — is the deferred store-split, codec §6.9; v1 seals logically.)
func (*Sealer) CancelSealed ¶
CancelSealed runs Cancel under the seal guard.
func (*Sealer) ConfirmSealed ¶
ConfirmSealed runs Confirm under the seal guard. The confirm cross-plane move (the hazardous one) is rejected if the booking's span touches a sealed day.
func (*Sealer) CreateSealed ¶
CreateSealed runs Create under the seal guard: a new booking cannot be created into the sealed past.
func (*Sealer) Frontier ¶
Frontier returns the current seal frontier (the instant before which days are sealed). Days whose end is at or before the frontier are immutable.
func (*Sealer) MoveSealed ¶
func (s *Sealer) MoveSealed(calendarID, bookingID string, to Span) (MoveResult, error)
MoveSealed runs Move under the seal guard, rejecting it if EITHER the current span or the destination touches a sealed day (you can move neither out of nor into the sealed past).
type Span ¶
Span is a concrete booking interval. Both endpoints are absolute UTC instants (xolutime.Instant); the span carries no zone or recurrence. It is half-open: [Start, End), so adjacent spans (one ending exactly where the next starts) do not overlap — matching the bitmap's quantum semantics.
type State ¶
type State string
State is an A9 lifecycle state.
const ( StateProposed State = "proposed" // tentative; occupies the proposed plane StateBinding State = "binding" // confirmed; occupies the binding plane StateHonoured State = "honoured" // completed; occupies the binding plane StateNotCommitted State = "not-committed" // declined proposal; occupies no plane StateCancelled State = "cancelled" // cancelled; occupies no plane StateMissed State = "missed" // sweeper-written non-occurrence; no plane )
type Store ¶
type Store interface {
PlaneBookingSource
CreateCalendar(c Calendar) (Calendar, error)
DeleteCalendar(calendarID string) error
PutBooking(b Booking) error
SetStateFrom(calendarID, bookingID string, from, to State) error
// contains filtered or unexported methods
}
Store is the full authoritative-record interface the Lifecycle engine drives: the calendar/booking CRUD plus the scoped queries. Both MemBookingSource and the SQLite-backed source implement it, so the lifecycle, commit, and seal logic (and every test) are storage-agnostic. It extends PlaneBookingSource (which extends BookingSource) with the mutating and lookup operations.