6.2 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||
|---|---|---|---|---|---|---|---|
| architecture | Pipeline Runner | How tenbackward.main synchronizes the blog, discovers anniversary candidates, applies deduplication, retries failures, and emits a run summary. |
|
2026-08-04T17:51:00Z |
Purpose
tenbackward.main is the entry point executed by cron. The pipeline first
ensures that the configured blog repository is available and current, then
scans Jekyll posts for today's ten-year anniversary, filters already-recorded
paths, publishes a single combined Mastodon status for the new matches, and
finally persists the freshly published identifiers.
Call Flow
main.main()
├── configure_json_logging()
├── load_config() ── raises ConfigError → exit 2
├── config.data_dir.mkdir(parents=True, exist_ok=True)
├── ensure_repo(config.data_dir, blog_repo_url, blog_dir, max_retries)
│ └── BlogRepoError → log blog_repo_error → exit 1
├── 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)
│ │ └── _run_once → ensure_repo → find_anniversary_matches
│ │ → PostedStore deduplication → publish_mastodon
│ │ → mark_posted_many # state only after publish OK
│ ├── 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[MatchedPost]: ...
_iter_candidates delegates to find_anniversary_matches() with
config.blog_dir / "_posts" / "blog" and config.site_url. The matcher
returns full MatchedPost values (relative path, title, date, canonical
URL); the runner uses MatchedPost.path as a stable deduplication ID and
passes the full objects to publish_mastodon() for status composition.
See Anniversary Matching for the
file and front matter rules.
Blog Synchronization
ensure_repo() runs before candidate discovery. An empty target is cloned
from BLOG_REPO_URL; an existing repository is fetched and fast-forwarded.
Transient Git failures use exponential backoff, while local modifications
fail immediately with BlogRepoError. The same synchronization occurs once
in main() before startup and again inside _run_once() as the retryable
pipeline boundary.
_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. |
PostedStore.mark_posted_many is called only when posted_ids
is non-empty, so a no-op run does not touch posted.json on disk.
The store persists the list under a "posted" key
({"posted": ["2014/2014-08-04-foo.md", ...]}) and serialises
concurrent runs with an fcntl.flock.
Publish Boundary
publish_mastodon() is the single success boundary. The runner calls it
after dedupe and before mark_posted_many. If the call raises a
PublishError, the runner treats it like any other pipeline exception —
the retry loop in _run_with_retry re-attempts, and no identifier is
written to posted.json. Identical-day matches are published as one
combined status: prefix line + one title\nurl block per post + hashtags
line, sorted by (date, path) for deterministic ordering. The default
prefix is Heute vor 10 Jahren: (overridable via THROWNBACK_PREFIX).
Generated statuses are validated against MASTODON_STATUS_LIMIT (500
characters); the runner fails safely (raises) rather than truncating.
Retry Behaviour (_run_with_retry)
attempts = max(1, config.max_retries)— at least one attempt even ifMAX_RETRIES=0.- The runner owns the unified retry budget;
_run_oncecallsensure_repowithmax_retries=0. - Transient Git and Mastodon failures are retried with delays of
2 ** (attempt - 1)seconds between attempts. - Each failed attempt emits
retry_attemptwithoperation,attempt, and the exceptionerrortext. - Fatal configuration, local repository, and publishing validation errors emit
retry_exhaustedimmediately without retrying. - When the budget is exhausted,
retry_exhaustedincludes the final operation, total attempts, and error text, andmainreturns exit code1.
Silent-on-No-Matches Contract
run_completeis emitted for a successful pass, including when all discovered candidates were already posted.- A run with zero candidates emits only
startup; norun_completeline is emitted.
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"].