Skip to content

Commit d4e4978

Browse files
committed
feat: add explicit source synchronization command
1 parent bc8c497 commit d4e4978

8 files changed

Lines changed: 36 additions & 16 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "basango"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
edition = "2024"
55
rust-version = "1.85"
66
description = "A Rust-native Basango news crawler with HTML and WordPress adapters"

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ sudo journalctl -fu basango-crawler-worker.service
4747

4848
The worker starts automatically at boot. Stopping it gracefully drains active work and leaves incomplete runs open so the next worker process can resume them. Repeated Redis connection failures make the process exit, allowing systemd to restart it without closing the affected runs.
4949

50+
### Synchronize sources
51+
52+
Run source registration and archive-size estimation once after installation, or again whenever the configured source list changes:
53+
54+
```bash
55+
cd /opt/crawler
56+
sudo -u basango ./crawler source
57+
```
58+
59+
Normal `crawl`, `schedule`, and `worker` runs do not synchronize or estimate sources.
60+
5061
### Schedule crawls (Initial)
5162

5263
Schedule every registered source that does not require an indexed category:

deploy/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ curl -fsSL https://raw.githubusercontent.com/bernard-ng/basango-crawler/refs/hea
4444

4545
From a local checkout, run `sudo ./deploy/uninstall.sh`. The script stops and disables the worker, removes its systemd unit, `/opt/crawler`, `/var/lib/crawler`, and the installer-created `basango` system account and group. It asks for confirmation first; pass `--yes` for unattended removal.
4646

47-
Pushing a `v*` Git tag runs the release workflow, which publishes native `aarch64` (Raspberry Pi) and `x86_64` Linux archives to the GitHub release.
47+
Pushing a release tag runs the release workflow, which publishes native `aarch64` (Raspberry Pi) and `x86_64` Linux archives to the GitHub release.
4848

4949
Each agent ID prefixes its discovery, article, and delivery queue names, so multiple Pis can safely share Redis. The worker consumes all three concurrently and reconciles SQLite delivery records after a restart. The installer does not schedule crawls. Run `crawler schedule` yourself or configure cron later with the sources and cadence assigned to that device.
5050

51+
After installation, register the configured sources and estimate their archive sizes once with `sudo -u basango /opt/crawler/crawler source`. Run it again only when the configured sources change; normal crawler and worker runs do not perform source synchronization.
52+
5153
To reset a Pi, stop its worker before clearing its scoped queues and SQLite outbox:
5254

5355
```bash

src/cli.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ struct Cli {
3333

3434
#[derive(Debug, Subcommand)]
3535
enum Command {
36+
/// Register configured sources and estimate their archive sizes.
37+
Source,
3638
Crawl(CrawlArgs),
3739
Schedule(ScheduleArgs),
3840
Worker(WorkerArgs),
@@ -119,6 +121,10 @@ pub async fn run() -> anyhow::Result<()> {
119121
.context("could not initialize crawler")?;
120122

121123
match cli.command {
124+
Command::Source => {
125+
crawler.synchronize_sources().await?;
126+
tracing::info!("source synchronization and estimation completed");
127+
}
122128
Command::Crawl(arguments) => {
123129
let report = crawler.crawl(arguments.into()).await?;
124130
tracing::info!(?report, "crawl completed");

src/crawler.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,11 @@ impl Crawler {
9999

100100
/// Crawl now, streaming collected drafts into the durable outbox.
101101
pub async fn crawl(&self, request: CrawlRequest) -> Result<CrawlReport> {
102-
self.runtime.synchronize_sources().await?;
103102
crawl_now(&self.runtime, request).await
104103
}
105104

106105
/// Schedule source discovery in BullMQ.
107106
pub async fn schedule(&self, mut request: CrawlRequest) -> Result<String> {
108-
self.runtime.synchronize_sources().await?;
109107
self.runtime.config.prepare_request(&mut request)?;
110108
let reporter = RunReporter::new(
111109
&self.runtime.config.ingestion,
@@ -159,10 +157,14 @@ impl Crawler {
159157

160158
/// Run BullMQ consumers until the process receives Ctrl-C.
161159
pub async fn work(&self, queues: Vec<String>, concurrency: usize) -> Result<()> {
162-
self.runtime.synchronize_sources().await?;
163160
run_worker(self.runtime.clone(), queues, concurrency).await
164161
}
165162

163+
/// Register configured sources and update their archive-size estimates.
164+
pub async fn synchronize_sources(&self) -> Result<()> {
165+
self.runtime.synchronize_sources().await
166+
}
167+
166168
/// Read the current agent's local outbox and Redis queue state.
167169
pub async fn status(&self) -> CrawlerStatus {
168170
let sqlite_path = self.runtime.config.sqlite_path();

src/execution.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ pub(crate) use worker::run_worker;
1818

1919
use std::sync::Arc;
2020

21-
use chrono::{DateTime, Utc};
22-
use serde::Deserialize;
23-
use tokio::sync::OnceCell;
24-
2521
use crate::{
2622
articles::endpoint_url,
2723
config::CrawlerConfig,
@@ -30,6 +26,8 @@ use crate::{
3026
http::HttpClient,
3127
telemetry::agent_id,
3228
};
29+
use chrono::{DateTime, Utc};
30+
use serde::Deserialize;
3331

3432
/// Shared, immutable dependencies are placed in `Arc` so queued jobs can own a
3533
/// cheap reference while running concurrently.
@@ -38,7 +36,6 @@ pub(crate) struct Runtime {
3836
pub config: Arc<CrawlerConfig>,
3937
pub http: HttpClient,
4038
pub agent_id: String,
41-
source_sync: Arc<OnceCell<()>>,
4239
}
4340

4441
impl Runtime {
@@ -50,15 +47,11 @@ impl Runtime {
5047
config: Arc::new(config),
5148
http,
5249
agent_id,
53-
source_sync: Arc::new(OnceCell::new()),
5450
})
5551
}
5652

5753
pub async fn synchronize_sources(&self) -> Result<()> {
58-
self.source_sync
59-
.get_or_try_init(|| async { source_sync::synchronize(self).await })
60-
.await
61-
.map(|_| ())
54+
source_sync::synchronize(self).await
6255
}
6356

6457
/// Ask the ingestion API for the last known article boundary when the caller did

tests/unit/cli.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ fn status_is_a_standalone_command() {
3030
assert!(matches!(cli.command, Command::Status));
3131
}
3232

33+
#[test]
34+
fn source_is_a_standalone_command() {
35+
let cli = Cli::try_parse_from(["crawler", "source"]).unwrap();
36+
assert!(matches!(cli.command, Command::Source));
37+
}
38+
3339
#[test]
3440
fn schedule_requires_an_explicit_source() {
3541
assert!(Cli::try_parse_from(["crawler", "schedule"]).is_err());

0 commit comments

Comments
 (0)