State & scheduling¶
Both pull actions are incremental: each run only fetches what's new since the last one. They achieve this
with state stored in Redis by IntegrationStateManager (app/services/state.py).
The state store¶
State is JSON, stored in REDIS_STATE_DB (default DB 0) under keys of the form:
source_id defaults to "no-source" when a single record covers the whole action. The API is small:
get_state (returns {} on miss), set_state, delete_state, and set_if_absent — an atomic
set-with-TTL used for the backfill lease. All calls retry on transient Redis errors.
Watermarks¶
Each pull action tracks a last_execution watermark:
| Situation | last_execution |
Window start used |
|---|---|---|
| First run | absent | start_datetime from config |
| Subsequent run | present | last_execution |
Catch-up (force_run_since_start = True) |
present | start_datetime (reset for one run) |
force_run_since_start resets the watermark for a single run — toggle it off afterward, or every run
re-pulls from start_datetime. end_datetime, when set, is sent on every run; clear it after a bounded
backfill so later runs don't recompute an empty window.
For pull_events, the watermark is compared against the ER timestamp chosen by filter_date_field
(default updated_at, which catches edits and backdated events).
Per-event state (pull_events)¶
pull_events keeps one record per ER event, keyed by the ER event UUID:
| Field | Purpose |
|---|---|
gundi_object_id |
The Gundi ID returned when the event was first posted — used to correlate later updates. |
updated_at |
Cached ER updated_at; if unchanged on the next sight, the event is skipped. |
state, priority, title |
Cached values, diffed to detect field changes. |
seen_note_ids |
IDs of notes already forwarded, so only new notes emit updates. |
On first sight, all existing notes are recorded as seen (no bulk-forward). See Data flow.
The resumable observation backfill¶
pull_observations is built to backfill large windows across many runs without dropping or duplicating
data. It stores a cursor under its state alongside last_execution:
{
"start": ..., "end": ..., # the overall window
"subwindow_days": ..., # slice width
"sources": [...], # sorted source UUIDs ([None] = no filter)
"window_index": ..., "source_index": ..., # progress within the (sub-window × source) grid
"no_progress_count": ..., # runaway guard for continue_immediately
"units_failed": ...,
}
The window is sliced into deterministic, half-open [start, end) sub-windows (_iter_subwindows), and the
action walks the (sub-window × source) grid, committing the cursor after each unit. Because the source
list is sorted and the slicing is deterministic, a resumed run regenerates the exact same unit sequence and
continues by index.
The watermark (last_execution) is advanced only when the whole window completes — so a run that fails
or times out mid-backfill leaves the watermark untouched and the next run resumes from the cursor.
Time budget and continuation¶
At ~80% of MAX_ACTION_EXECUTION_TIME, the run saves its cursor and stops. Continuation is either:
- Scheduler-driven (default) — the next scheduled tick picks up the cursor, or
- Immediate (
continue_immediately = True) — the run re-triggers the next chunk via theINTEGRATION_COMMANDS_TOPICPubSub topic. A runaway guard stops self-re-triggering after 3 consecutive runs that make no progress.
The backfill lease¶
Because a backfill can outlast its schedule interval, two runs could otherwise overlap and race on the
cursor (causing duplicate sends). Before working, the action takes a lease via set_if_absent with a TTL of
MAX_ACTION_EXECUTION_TIME + 30s. If the lease is already held, the run skips quietly. The lease is released
in a finally block, with the TTL as a backstop. Lease acquisition fails open — if Redis is briefly
unavailable, the run proceeds rather than crash (a rare duplicate is cheaper than a stall).
Scheduling and run_on_schedule¶
Schedules are attached with @crontab_schedule("…") or register.py --schedule. But both pull actions
default run_on_schedule to False: a scheduled tick for a pull action whose config has
run_on_schedule off (or missing/invalid config) is skipped quietly, not errored. This integration is
most often deployed only as a destination, so pulling is opt-in per connection. Manual runs always
execute regardless.