4.2 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||
|---|---|---|---|---|---|---|---|
| architecture | Pipeline Runner | How tenbackward.main turns one cron tick into a startup log line, a retry-wrapped pipeline pass, and a single run_complete summary line. |
|
2026-08-04T17:51:00Z |
Purpose
tenbackward.main is the entry point executed by cron. Job 1083
rewrote it to: (a) install JSON logging, (b) execute a retry-wrapped
pipeline pass that is silent when there are no candidates, and
(c) emit a single structured run_complete summary on success.
Call Flow
main.main()
├── configure_json_logging()
├── load_config() ── raises ConfigError → exit 2
├── config.data_dir.mkdir(parents=True, exist_ok=True)
├── log_startup(version, site_url, run_at, tz, hashtags, throwback_prefix)
├── result = _run_with_retry(config)
│ ├── for attempt in 1 .. max_retries+1:
│ │ try: return _run_once(config)
│ │ except Exception as exc:
│ │ log_error("pipeline_error", exc=exc, attempt=attempt, max_attempts=attempts)
│ ├── log_error("pipeline_failed", exc=last_exc, attempts=attempts)
│ └── return None
├── if result is None: return 1
└── log_run_summary(scanned, matched, posted, skipped, posted_ids)
Extension Point: _iter_candidates
def _iter_candidates(config: Config) -> Iterable[str]: ...
Job 1083 ships _iter_candidates as an empty iterator so the
silent-on-no-matches contract is the default behaviour. The future
blog-clone + matching pipeline plugs into this function without
touching _run_once or _run_with_retry.
_run_once — Summary Counters
_run_once returns a 5-tuple:
(scanned, matched, posted, skipped, posted_ids)
| Counter | Meaning |
|---|---|
scanned |
Items yielded by _iter_candidates. |
matched |
Items that survived the dedupe-vs-already_posted check. |
posted |
Items newly recorded in posted.json during this run. |
skipped |
Items that matched but were already in posted.json (deduped). |
posted_ids |
The list of post identifiers written to state. |
state.save_posted is called only when posted_ids is non-empty,
so a no-op run does not touch posted.json on disk.
Retry Behaviour (_run_with_retry)
attempts = max(1, config.max_retries + 1)— at least one attempt even ifMAX_RETRIES=0.- Any exception inside
_run_onceis caught (noqa: BLE001— intentional, the whole pass is opaque to the runner). - Each failed attempt is recorded via
log_error("pipeline_error", exc=exc, attempt=attempt, max_attempts=attempts). - When the budget is exhausted, the runner emits
log_error("pipeline_failed", exc=last_exc, attempts=attempts)and returnsNonesomaincan translate it toexit 1. time.sleep(0)between attempts is the placeholder seam where the next job can introduce real backoff.
Silent-on-No-Matches Contract
A run with zero candidates produces exactly one line:
{"ts":"...","level":"INFO","message":"startup","event":"startup",...}
No run_complete line is emitted. Tests assert this by counting log
records (tests/test_run_logging.py::test_run_summary_silent_when_no_candidates).
Example Sequence (Healthy Run)
startupline at INFO._run_with_retryreturns(0, 0, 0, 0, [])(no candidates yet because_iter_candidatesis empty) →mainexits0without arun_completeline.- Next-day tick repeats the cycle.
Example Sequence (Future Blog Step in Place)
startupline at INFO._iter_candidatesyields[id-1, id-2].id-1is already inposted.json, soskipped += 1.id-2is new and appended toposted_idsand persisted.run_completeline at INFO withscanned=2, matched=2, posted=1, skipped=1, posted_ids=["id-2"].