Skip to content

feat: integrate mailing list service (CM-1318)#4346

Open
themarolt wants to merge 17 commits into
mainfrom
feat/mailing-list-integration-CM-1318
Open

feat: integrate mailing list service (CM-1318)#4346
themarolt wants to merge 17 commits into
mainfrom
feat/mailing-list-integration-CM-1318

Conversation

@themarolt

Copy link
Copy Markdown
Contributor

Summary

Integrate mailing list (public-inbox) support into CDP as a containerized worker. This implements email ingestion from public-inbox repositories (like LKML), mirroring shards, parsing emails, and emitting activities to Kafka for the data sink.

  • New mailing_list_integration service (Python, FastAPI)
  • Database schema for list management and processing state
  • Public-inbox integration via subprocess (mirrors, incremental fetch)
  • Email parser ported from noteren
  • Kafka producer for activity ingestion
  • Docker image for containerized deployment

How it works

  1. Worker polls mailinglist.listProcessing with FOR UPDATE SKIP LOCKED
  2. For each list, calls public-inbox-clone (initial) or public-inbox-fetch (incremental)
  3. Discovers shards (0.git, 1.git, etc.) and walks new commits since last processed
  4. Parses each email blob, emits groupsio-platform activity JSON
  5. Inserts results into integration.results (state=pending)
  6. Sends Kafka message to data-sink-worker for activity processing
  7. Updates lastProcessedHeads (per-shard tracking) and marks list as COMPLETED

Test coverage

  • 54 pytest tests (email parser) remain green
  • Ruff lint clean on all new Python files
  • Database schema migrates cleanly
  • Server imports and FastAPI lifespan tested

Out of scope

  • Onboarding UI to create integrations + mailinglist.lists rows
  • Member join/leave derivation (message ingestion only initially)

This PR targets hardcoded integrations for initial deployment; UI follows in a separate ticket.

themarolt added 11 commits July 14, 2026 18:50
Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
Copilot AI review requested due to automatic review settings July 15, 2026 10:56
@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches activity ingestion (member identity handling) and adds a new integration path with schema migrations; core auth unchanged but duplicate-member behavior changes for mailing-list-style identities.

Overview
Adds end-to-end mailing list ingestion for lore/public-inbox archives: a new Python mailing_list_integration worker mirrors lists, parses emails, writes integration.results, and emits Kafka messages for data-sink-worker.

Backend & data: New mailinglist schema (lists, listProcessing with per-shard heads), message activity type for platform mailinglist, PUT /mailing-list-connect (tenant edit), and IntegrationService.mailingListConnectOrUpdate with DAL upsertMailingLists. Dev scripts onboard the default tenant and create integrations from JSON until UI ships.

Worker: FastAPI + poll loop with SKIP LOCKED acquisition, public-inbox-clone/fetch, ported noteren parser (skips implausible/unparseable dates per ADR-0009), and Docker/compose/CI ruff for the new app.

Ingestion hardening: data_sink_worker lowercases member dedupe keys and, on verified identity conflicts during create, attaches to the existing member instead of failing; handles an additional unique constraint.

Reviewed by Cursor Bugbot for commit f521e7a. Bugbot is set up for automated code reviews on this repo. Configure here.

git_blob_id,
git_dir,
)
sys.exit(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Git errors terminate worker

High Severity

When a commit or blob is missing in a shard, get_email_from_git calls sys.exit(1). The list worker invokes that via read_email, and except Exception does not catch SystemExit, so one bad commit can exit the whole FastAPI/uvicorn process instead of marking the list failed and releasing the lock.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 212875a. Configure here.

await queue_service.send_batch_activities(activities_kafka)

await update_processed_heads(mailing_list.id, heads)
state = ListState.COMPLETED

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kafka failure orphans DB rows

High Severity

Activities are inserted into integration.results before Kafka emit succeeds. If send_batch_activities fails afterward, the list is marked failed and shard heads are not advanced, but pending rows remain and the data-sink worker never receives process_integration_result messages for them.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 212875a. Configure here.


if activities_db:
await batch_insert_activities(activities_db)
await queue_service.send_batch_activities(activities_kafka)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Onboarding buffers entire shard

High Severity

First-time processing walks every commit in each shard and appends all activities into in-memory lists before a single DB insert and Kafka batch. Large lore lists can exhaust memory or run far beyond practical limits, unlike chunked processing in git_integration.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 212875a. Configure here.

logger.info(f"Saving {len(records)} activities into integration.results")
for i in range(0, len(records), batch_size):
batch = records[i : i + batch_size]
await executemany(sql_query, batch)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Activity batches not transactional

Medium Severity

batch_insert_activities issues multiple executemany calls without a shared transaction. If a later batch fails, earlier batches may already be committed, leaving partial data and no matching Kafka emit for the full run.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 212875a. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a containerized Python worker for ingesting public-inbox mailing lists into CDP through Kafka and integration.results.

Changes:

  • Adds mirroring, email parsing, processing, and Kafka publishing.
  • Adds mailing-list database schema and development seed.
  • Adds container, Compose, CI, and local tooling.

Review note: This exceeds the recommended 1,000-line PR target.

Reviewed changes

Copilot reviewed 26 out of 35 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
services/apps/mailing_list_integration/src/test/test_noteren.py Adds parser and CLI tests.
services/apps/mailing_list_integration/src/runner.sh Starts Uvicorn.
services/apps/mailing_list_integration/src/crowdmail/worker/list_worker.py Implements ingestion loop.
services/apps/mailing_list_integration/src/crowdmail/worker/__init__.py Initializes worker package.
services/apps/mailing_list_integration/src/crowdmail/settings.py Defines environment configuration.
services/apps/mailing_list_integration/src/crowdmail/services/queue/queue_service.py Adds Kafka producer.
services/apps/mailing_list_integration/src/crowdmail/services/queue/__init__.py Initializes queue package.
services/apps/mailing_list_integration/src/crowdmail/services/parse/noteren.py Parses public-inbox emails.
services/apps/mailing_list_integration/src/crowdmail/services/parse/__init__.py Initializes parser package.
services/apps/mailing_list_integration/src/crowdmail/services/mirror/mirror_service.py Manages public-inbox mirrors.
services/apps/mailing_list_integration/src/crowdmail/services/mirror/__init__.py Initializes mirror package.
services/apps/mailing_list_integration/src/crowdmail/services/__init__.py Initializes services package.
services/apps/mailing_list_integration/src/crowdmail/server.py Adds FastAPI lifecycle and health route.
services/apps/mailing_list_integration/src/crowdmail/models/list.py Defines mailing-list model.
services/apps/mailing_list_integration/src/crowdmail/models/__init__.py Initializes models package.
services/apps/mailing_list_integration/src/crowdmail/logger.py Configures Loguru.
services/apps/mailing_list_integration/src/crowdmail/errors.py Defines service errors.
services/apps/mailing_list_integration/src/crowdmail/enums.py Defines service enums.
services/apps/mailing_list_integration/src/crowdmail/database/registry.py Adds database helpers.
services/apps/mailing_list_integration/src/crowdmail/database/crud.py Adds processing-state queries.
services/apps/mailing_list_integration/src/crowdmail/database/connection.py Manages asyncpg pooling.
services/apps/mailing_list_integration/src/crowdmail/database/__init__.py Initializes database package.
services/apps/mailing_list_integration/src/crowdmail/__init__.py Initializes application package.
services/apps/mailing_list_integration/README.md Documents operation and setup.
services/apps/mailing_list_integration/pyproject.toml Defines package and tooling.
services/apps/mailing_list_integration/Makefile Adds development commands.
services/apps/mailing_list_integration/dev/seed.sql Seeds an example integration.
scripts/services/mailing-list-integration.yaml Adds Compose services.
scripts/services/docker/Dockerfile.mailing_list_integration Builds the worker image.
scripts/cli Registers the service in clean-start handling.
scripts/builders/mailing-list-integration.env Configures image publishing.
backend/src/database/migrations/V1784048135__mailinglist-schema.sql Creates mailing-list tables.
.github/workflows/backend-lint.yaml Adds Python linting CI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

RUN apt-get update && apt-get install -y \
ca-certificates \
git \
public-inbox \
Comment on lines +32 to +33
class ListWorker:
"""Worker that mirrors, parses and emits activities for mailing lists"""
shard = shard_index(shard_path)
commit_ids = await new_commits(shard_path, heads.get(shard))
for git_id in commit_ids:
message, blob_id = read_email(shard_path, git_id)
Comment on lines +88 to +89
activities_db = []
activities_kafka = []
version = "0.0.1"
description = "Crowd.dev mailing list integration for the Linux Foundation"
readme = "README.md"
license = { file = "LICENSE" }
Comment on lines +155 to +163
futures = [
_producer.send(
topic=CROWD_KAFKA_TOPIC,
key=activity["message_id"].encode("utf-8", errors="replace"),
value=activity["payload"].encode("utf-8", errors="replace"),
)
for activity in activities_kafka
]
await asyncio.gather(*futures, return_exceptions=False)
Comment on lines +81 to +87
else:
logging.error(
'Unable to retrieve git blob %s from the git repo "%s"',
git_blob_id,
git_dir,
)
sys.exit(1)
Comment on lines +139 to +141
- name: Install dependencies
if: steps.changes.outputs.python_changed == 'true'
run: uv sync --group dev --frozen
)
# add a fake set of microseconds just to make date parsers on the injest side happier
date = date_dt.strftime("%Y-%m-%dT%H:%M:%S") + ".000000Z"
except (ValueError, OverflowError):
Signed-off-by: Uroš Marolt <uros@marolt.me>
…ion (CM-1318)

Signed-off-by: Uroš Marolt <uros@marolt.me>
Signed-off-by: Uroš Marolt <uros@marolt.me>
…318)

Signed-off-by: Uroš Marolt <uros@marolt.me>
Copilot AI review requested due to automatic review settings July 20, 2026 09:17
logger.info("Worker shutdown complete")
except TimeoutError:
logger.warning("Worker shutdown timeout, forcing cancellation")
worker_task.cancel()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shutdown waits on null task

Low Severity

The lifespan finally block always calls asyncio.wait_for(worker_task, …) and may call worker_task.cancel(), but worker_task stays None if asyncio.create_task fails during startup. That raises TypeError during shutdown and can mask the original startup error.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 037a969. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 51 changed files in this pull request and generated 16 comments.

Comments suppressed due to low confidence (3)

services/apps/mailing_list_integration/src/crowdmail/worker/list_worker.py:90

  • Initial onboarding materializes every commit ID and retains every parsed DB/Kafka record until the entire archive finishes. For LKML-scale archives this is unbounded memory usage and no progress is checkpointed before a likely OOM/restart. Process bounded commit batches and persist each batch/head checkpoint before continuing, similar to the 250-commit chunking in git_integration/src/crowdgit/services/commit/commit_service.py:709-743.
    .github/workflows/backend-lint.yaml:147
  • This CI job installs pytest and the PR adds a substantial test suite, but the job runs only Ruff. As a result, parser regressions can merge even though the PR reports the tests as coverage. Run pytest in this job after lint/format checks.
      - name: Check Python linting and formatting
        if: steps.changes.outputs.python_changed == 'true'
        run: |
          uv run ruff check src/ --output-format=github
          uv run ruff format --check src/

services/apps/mailing_list_integration/src/crowdmail/worker/list_worker.py:31

  • This introduces a new class-based worker, contrary to the repository's functions-over-classes direction for new service code (CLAUDE.md:42-43 and services-checklist.md:41-43). Keep shutdown state in a small functional runner/context and expose plain processing functions so the polling and per-list logic remain independently testable.

state = ListState.FAILED

try:
list_dir = await ensure_mirror(mailing_list.name)
lists: z
.array(
z.object({
name: z.string().trim().min(1),
Comment on lines +92 to +94
for git_id in commit_ids:
message, blob_id = read_email(shard_path, git_id)
parsed = parse_email(
Comment on lines +135 to +137
def read_email(shard_path: str, git_id: str) -> tuple[bytes, str]:
"""Raw email bytes + blob id for a commit in a shard."""
return get_email_from_git(shard_path, git_id)
Comment on lines +164 to +166
# Retried on any failure: if this write never lands, the list stays stuck
# in its acquired state (e.g. PROCESSING) forever, since acquire_onboarding_list
# only picks PENDING and acquire_recurrent_list excludes PROCESSING.
psql "$CROWD_DB_WRITE_URI" -f dev/seed.sql
```

It creates a `public.integrations` row (`platform='groupsio'`), a
Comment on lines +61 to +62
- emits one Kafka message per result (`process_integration_result`,
`platform=groupsio`),
Comment on lines +65 to +66
4. Confirm `data_sink_worker` consumes the message, resolves the integration's
`platform='groupsio'`, and creates the member (email-based verified
Comment on lines +72 to +73
- Onboarding UI/backend to create `integrations` + `mailinglist.lists` rows
(this is what `dev/seed.sql` stands in for).
# of emitting a malformed timestamp.
if not 1970 <= date_dt.year <= 9999:
raise ValueError(f"implausible year {date_dt.year}")
# add a fake set of microseconds just to make date parsers on the injest side happier
Copilot AI review requested due to automatic review settings July 20, 2026 16:49
await _run_shell_command(["public-inbox-fetch", "-q"], cwd=list_dir)
else:
logger.info(f"Fetching updates for lore list '{list_name}'")
await _run_shell_command(["public-inbox-fetch", "-q", "-C", list_dir])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mirror ignores stored source URL

Medium Severity

Onboarding persists a per-list sourceUrl, but mirroring always clones or fetches https://lore.kernel.org/{name}/ using only the list name. A list whose sourceUrl is not that lore path is mirrored from the wrong host/path while activities still record the stored sourceUrl, yielding incorrect or empty ingestion.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cf4735c. Configure here.

- name: Check Python linting and formatting
if: steps.changes.outputs.python_changed == 'true'
run: |
uv run ruff check src/ --output-format=github

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Git integration CI drops format check

Low Severity

The lint-python-git-integration job’s lint step now runs only ruff check. The previous ruff format --check for that job was removed when the mailing-list lint job was added, so unformatted Python under git_integration can pass CI.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cf4735c. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 51 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (10)

services/apps/mailing_list_integration/src/crowdmail/worker/list_worker.py:84

  • This ignores the stored source_url; ensure_mirror always clones https://lore.kernel.org/{name}/. A valid public-inbox URL with a different host or path will silently ingest the wrong archive. Pass the source URL to the mirror layer and keep the local directory key separate.
    services/apps/mailing_list_integration/src/crowdmail/worker/list_worker.py:91
  • On initial onboarding, new_commits(..., None) materializes the shard’s entire history, and this method then retains every DB and Kafka record until all shards finish. Public-inbox shards can contain hundreds of thousands of messages, so the worker can exhaust memory before reaching the existing insert chunks. Iterate in bounded batches and checkpoint each batch only after persistence and emission succeed.
    services/apps/mailing_list_integration/src/crowdmail/worker/list_worker.py:93
  • read_email launches synchronous Git subprocesses on the FastAPI event loop—twice per message. A large import will block health requests, shutdown handling, and other async work for long periods. Offload this blocking call to a thread.
    services/apps/mailing_list_integration/src/crowdmail/worker/list_worker.py:94
  • Any parser exception escapes this per-commit loop, marks the whole list failed, and leaves the shard head unchanged. The next retry starts from the same malformed message, so one poison email can permanently block every later message in the shard. Catch failures per commit and skip/quarantine them with observable logging or metrics, as the Git integration does in commit_service.py:652-655.
    services/apps/mailing_list_integration/src/crowdmail/services/parse/noteren.py:72
  • This helper is called by the long-running worker, so sys.exit() raises SystemExit, bypasses its except Exception handlers, and can terminate the service because of one malformed commit. Raise a domain exception and let the worker apply its per-message failure policy instead.
    services/apps/mailing_list_integration/src/crowdmail/services/parse/noteren.py:87
  • The blob-read failure also calls sys.exit(). In worker mode this can terminate the entire service instead of failing one message/list. Raise a normal domain exception here, consistent with the commit-not-found branch.
    services/apps/mailing_list_integration/src/crowdmail/services/parse/noteren.py:354
  • Messages without Message-ID are emitted with an empty sourceId. Activity deduplication keys include sourceId, so multiple such messages in the same segment/platform/type/channel can collapse into one activity. Use a stable fallback such as the Git commit/blob ID, or explicitly skip these messages.
    services/apps/mailing_list_integration/src/crowdmail/services/mirror/mirror_service.py:58
  • The five-minute default applies to initial public-inbox-clone and fetch operations. LKML-scale archives can legitimately take much longer, so onboarding will repeatedly kill the clone and never complete. Use a separately configurable mirror timeout sized for large archives while retaining shorter limits for lightweight commands.
    services/apps/mailing_list_integration/src/crowdmail/database/crud.py:117
  • A process crash after acquisition leaves state='processing' and lockedAt set. This exclusion, combined with both selectors requiring lockedAt IS NULL, means the list is never acquired again; stale onboarding rows also consume the concurrency count forever. Treat locks as leases and reclaim/reset processing rows older than a configured threshold.
    states_to_exclude = (ListState.PENDING, ListState.PROCESSING, ListState.PENDING_REONBOARD)

services/apps/mailing_list_integration/README.md:16

  • This documentation still says the new worker emits platform=groupsio, while the implementation, seed, activity type, and identities all use platform=mailinglist. The same stale Groups.io references recur at lines 46, 62, and 66; update them so operators verify the actual platform.
  parses new messages, writes activities to `integration.results`, and emits
  Kafka messages to the `data-sink-worker` topic — same plumbing as
  git_integration, with `platform=groupsio`.

// find members to create
for (const payload of payloadsWithoutDbMembers) {
const key = `${payload.platform}:${payload.activity.username}`
const key = `${payload.platform}:${payload.activity.username?.toLowerCase()}`
// find object members to create
for (const payload of payloadsWithoutDbObjectMembers) {
const key = `${payload.platform}:${payload.activity.objectMemberUsername}`
const key = `${payload.platform}:${payload.activity.objectMemberUsername?.toLowerCase()}`
"channel": channel,
"title": subject,
"body": json_body,
"url": "https://lore.kernel.org/r/" + msgid,
Comment on lines +78 to +80
def list_mirror_dir(list_name: str, mirror_dir: str = LORE_MIRROR_DIR) -> str:
"""Local directory where a lore list mirror lives."""
return os.path.join(mirror_dir, list_name)
Comment on lines +2 to +3
INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration", description) VALUES
('message', 'mailinglist', false, true, 'Sent a message to a mailing list');
Copilot AI review requested due to automatic review settings July 21, 2026 19:35

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 9 total unresolved issues (including 7 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f521e7a. Configure here.

// find members to create
for (const payload of payloadsWithoutDbMembers) {
const key = `${payload.platform}:${payload.activity.username}`
const key = `${payload.platform}:${payload.activity.username?.toLowerCase()}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lowercase keys, case-sensitive match

Medium Severity

Member batching now keys membersToCreateMap with a lowercased platform:username, but success and error paths still match payloads with case-sensitive username equality. Activities for the same mailing-list address with different casing can land in one batch yet fail identity-error handling or miss shared memberId assignment.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f521e7a. Configure here.

state = ListState.FAILED

try:
list_dir = await ensure_mirror(mailing_list.name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mirror ignores list source URL

Medium Severity

Processing clones and fetches using mailing_list.name against a hardcoded lore.kernel.org base URL, while onboarding stores a separate sourceUrl that is only passed into the parser. A valid sourceUrl with a mismatched name mirrors the wrong list or fails while activities claim a different source.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f521e7a. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 51 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (11)

services/apps/data_sink_worker/src/service/activity.service.ts:1165

  • Lowercasing only this aggregation key is inconsistent with the exact-case comparison used when assigning the created member ID (line 1243). If a batch contains the same identity with different casing, the entries are grouped under the first spelling, but payloads using the other spelling never receive memberId. Normalize the later comparisons too, or keep this key case-sensitive.
      const key = `${payload.platform}:${payload.activity.username?.toLowerCase()}`

services/apps/data_sink_worker/src/service/activity.service.ts:1191

  • This has the same partial-normalization problem as the actor path: object members are grouped case-insensitively here, but line 1254 maps the result back with an exact-case comparison. Case variants grouped into one entry can therefore leave some payloads without objectMemberId.
      const key = `${payload.platform}:${payload.activity.objectMemberUsername?.toLowerCase()}`

services/apps/mailing_list_integration/src/crowdmail/services/queue/queue_service.py:164

  • AIOKafkaProducer.send() returns a delivery future. This gather waits only for each send coroutine to enqueue its record and receives those futures as results; it does not wait for broker acknowledgements. A broker rejection can therefore go unnoticed while the worker advances its shard heads. Await send_and_wait() (or await every returned delivery future) before reporting success.
    services/apps/mailing_list_integration/src/crowdmail/worker/list_worker.py:84
  • mailing_list.name is API-controlled and ensure_mirror() uses it both as a filesystem path component and as the remote lore slug, while the stored source_url is ignored. A name such as ../../tmp/list escapes LORE_MIRROR_DIR, and a valid source URL that differs from the name clones the wrong inbox. Use an opaque local key such as the list ID and pass a validated/allowlisted source URL separately as the clone remote.
    services/apps/mailing_list_integration/src/crowdmail/database/crud.py:99
  • A process crash after acquisition leaves the row in processing with lockedAt set. Onboarding only selects pending, while this recurrent query excludes processing and also requires an empty lock, so that list can never be acquired again. Add lease expiry/stale-lock recovery with a timeout safely longer than a valid processing run.
        WHERE NOT (lp.state = ANY($2))
            AND lp."lockedAt" IS NULL
            AND l."deletedAt" IS NULL

services/libs/data-access-layer/src/mailinglist/index.ts:38

  • This conflict target makes a source URL global across all segments. Since mailing-list integrations are created per segment, onboarding the same public list in a second segment reassigns the existing row to the second integration while preserving the first integration's processed heads: the first segment stops receiving mail and the second misses history. Scope uniqueness to the integration/segment and create a separate processing row per onboarding.
    services/apps/mailing_list_integration/src/crowdmail/services/mirror/mirror_service.py:137
  • get_email_from_git() calls sys.exit() on retrieval errors, and this worker-facing wrapper lets SystemExit escape. Because SystemExit is not an Exception, it bypasses all of the worker's error handlers and one malformed shard commit can terminate the service. The parser helper should raise a typed exception; only the standalone CLI boundary should translate that into an exit code.
    .github/workflows/backend-lint.yaml:147
  • This new CI job installs the test dependencies but only runs Ruff; no workflow runs the added pytest suite. Parser and CLI regressions can therefore merge despite the PR's stated test coverage. Run pytest in this job whenever the service's Python files change.
      - name: Check Python linting and formatting
        if: steps.changes.outputs.python_changed == 'true'
        run: |
          uv run ruff check src/ --output-format=github
          uv run ruff format --check src/

services/apps/mailing_list_integration/src/crowdmail/worker/list_worker.py:34

  • This introduces a new class-based worker despite the repository's current convention to write new services/workers as plain functions; class-based services are explicitly legacy (CLAUDE.md:42-43, services review checklist §8). Keeping lifecycle state in a small closure/module and exposing run/shutdown functions would make this worker composable and independently testable.
    services/apps/mailing_list_integration/README.md:48
  • The implementation and dev seed use the new mailinglist platform, but this README still instructs users to create and verify a groupsio integration (also repeated later in the end-to-end steps). Following these instructions produces an integration whose platform does not match the emitted activities. Update all remaining groupsio references in this README to mailinglist.
It creates a `public.integrations` row (`platform='groupsio'`), a
`mailinglist.lists` row pointing at an existing segment, and a
`mailinglist."listProcessing"` row in state `pending` — which the worker's

services/apps/mailing_list_integration/src/crowdmail/worker/list_worker.py:87

  • An initial public-inbox shard can contain millions of messages, but this retains every parsed body and Kafka payload in memory until every shard has been scanned; new_commits() also returns the full commit list. Large lists such as LKML will exhaust container memory before the existing DB batching runs. Process bounded commit/activity batches and checkpoint each durable batch instead.

Comment on lines +131 to +135
if activities_db:
await batch_insert_activities(activities_db)
await queue_service.send_batch_activities(activities_kafka)

await update_processed_heads(mailing_list.id, heads)
if line.find("From: ") == 0:
author_name, author_email = parse_author(line)

if line.find(">") != 0 and line.find("On ") != 0 and line.find("From: ") != 0:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants