Skip to content

Add aggregated Postgres writer for status change topic#189

Open
kevinwallimann wants to merge 13 commits into
masterfrom
feature/status-change-aggregation
Open

Add aggregated Postgres writer for status change topic#189
kevinwallimann wants to merge 13 commits into
masterfrom
feature/status-change-aggregation

Conversation

@kevinwallimann

@kevinwallimann kevinwallimann commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Overview

  • Added a Postgres writer for the status_change topic
  • Added ADR explaining merge strategy for event aggregation, handling out-of-order and duplicate events
  • Updated test config to work with colima on macOS

Release Notes

  • Add aggregated Postgres writer for status change topic

Related

Closes #188

Summary by CodeRabbit

  • New Features
    • Added Postgres support for job status-change events, aggregating lifecycle timestamps, retries, nested jobs, and additional context into a single job status record.
  • Bug Fixes
    • Improved consistency when processing out-of-order and duplicate status-change events, ensuring correct terminal status and idempotent updates.
    • Added resilient Postgres container startup retries for integration tests on macOS Docker (colima).
  • Tests
    • Added integration tests covering ordering, idempotency, retry behavior, and nested jobs for status-change persistence.
  • Documentation
    • Documented the status-change event bus “status change database” approach and updated integration test setup notes.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@kevinwallimann, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 007753be-68c2-48ca-bf41-486016c957e0

📥 Commits

Reviewing files that changed from the base of the PR and between 00c38f9 and 5dc0990.

📒 Files selected for processing (2)
  • adr/001-status-change-db/001-status-change-db.md
  • src/writers/sql/inserts.sql

Walkthrough

Adds Postgres aggregation for status_change events through a documented job schema, merge-style upsert query, writer dispatch path, access and integration-schema updates, container connection retries, and integration tests.

Changes

Status Change Aggregation

Layer / File(s) Summary
Database design
adr/001-status-change-db/001-status-change-db.md
Documents the job table, event merge rules, timestamp handling, and query patterns.
Upsert contract and topic wiring
src/writers/sql/inserts.sql, src/utils/constants.py
Adds conditional upsert_status_change logic and enables the status-change topic for Postgres writes.
Writer aggregation path
src/writers/writer_postgres.py
Converts timestamps, maps event types to lifecycle fields, serializes context, and dispatches status-change events.
Database and test setup
tests/integration/schemas/postgres_schema.py, conf/access.json, tests/integration/conftest.py, DEVELOPER.md
Adds the aggregated-job schema, expands topic access, retries Postgres startup connections, and documents colima setup.
Integration coverage
tests/integration/test_status_change_writer.py
Tests creation, ordering, merging, nested jobs, retries, and idempotency.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant EventGate
  participant WriterPostgres
  participant Postgres
  Client->>EventGate: Send status_change event
  EventGate->>WriterPostgres: Dispatch TOPIC_STATUS_CHANGE
  WriterPostgres->>WriterPostgres: Map event and serialize context
  WriterPostgres->>Postgres: Execute upsert_status_change
  Postgres-->>WriterPostgres: Insert or update aggregated job row
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: petr-pokorny-absa, oto-macenauer-absa, tmikula-dev

Poem

A rabbit hops where status flows,
One job row gathers what it knows,
Events merge, timestamps agree,
Postgres keeps the history,
With nested hops and retries bright—
🐇 One tidy table by moonlight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding an aggregated Postgres writer for the status_change topic.
Description check ✅ Passed The description includes an overview, release notes, and the related issue reference required by the template.
Linked Issues check ✅ Passed The PR implements the requested status_change aggregation writer and persistence table described in #188.
Out of Scope Changes check ✅ Passed No obvious unrelated changes stand out; the ADR, test config, schema, writer, and tests all support the feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/status-change-aggregation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kevinwallimann
kevinwallimann force-pushed the feature/status-change-aggregation branch from a7f6b26 to 83fb752 Compare July 8, 2026 08:57
@kevinwallimann
kevinwallimann marked this pull request as ready for review July 8, 2026 08:59

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
adr/001-status-change-db/001-status-change-db.md (1)

51-59: 🩺 Stability & Availability | 🔵 Trivial

Verify FK constraints don't break out-of-order parent-child event processing.

The ADR defines FK constraints on parent_job_id and initial_job_id, but also states events may be received out-of-order during failure scenarios. If a child job event arrives before its parent event, the FK constraint would reject the insert. The integration test schema intentionally omits these constraints, so this scenario is untested. Confirm whether parent-child event ordering is guaranteed in production, or whether the FK constraints should be deferred or removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adr/001-status-change-db/001-status-change-db.md` around lines 51 - 59, The
FK constraints on parent_job_id and initial_job_id in the ADR may conflict with
out-of-order event arrival, so review the intended production ordering
guarantees before keeping them as immediate constraints. Update the
schema/design in the ADR to either state that parent events always exist first,
or change the FK approach to deferred enforcement or remove the constraints;
reference the job relationship constraints and the event processing flow
described in the status-change DB design.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@adr/001-status-change-db/001-status-change-db.md`:
- Around line 51-59: The FK constraints on parent_job_id and initial_job_id in
the ADR may conflict with out-of-order event arrival, so review the intended
production ordering guarantees before keeping them as immediate constraints.
Update the schema/design in the ADR to either state that parent events always
exist first, or change the FK approach to deferred enforcement or remove the
constraints; reference the job relationship constraints and the event processing
flow described in the status-change DB design.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 548c66cc-bac2-47cc-949d-a932af83f3d8

📥 Commits

Reviewing files that changed from the base of the PR and between 7e2f168 and 83fb752.

📒 Files selected for processing (9)
  • DEVELOPER.md
  • adr/001-status-change-db/001-status-change-db.md
  • conf/access.json
  • src/utils/constants.py
  • src/writers/sql/inserts.sql
  • src/writers/writer_postgres.py
  • tests/integration/conftest.py
  • tests/integration/schemas/postgres_schema.py
  • tests/integration/test_status_change_writer.py

finished_at TIMESTAMPTZ,
last_updated_at TIMESTAMPTZ NOT NULL,

CONSTRAINT fk_job_parent

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.

These are not implemented anywhere.

BTW, this project does not treat DB as a first class citizen. I think that someone just manually created tables on the DB. It cannot continue like this, I've created a ticket here: #201

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Perfect, thanks! I imagined something similar like in Atum-Service. Great to see that it already exists there

Comment thread adr/001-status-change-db/001-status-change-db.md Outdated
platform_metadata JSONB,
input_arguments JSONB,
additional_context JSONB,
attempt_number INTEGER NOT NULL CHECK (attempt_number > 0),

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.

Suggested change
attempt_number INTEGER NOT NULL CHECK (attempt_number > 0),
attempt_number INTEGER NOT NULL DEFAULT 1 CHECK (attempt_number > 0),

just for the sake of consistency

);

-- Table for test_status_change_writer
CREATE TABLE IF NOT EXISTS public_cps_za_status_change_aggregated_job (

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.

[you can ignore]

I've actually never liked the decision to have all tables within the common public schema & together. But I don't want to migrate now as it's low value and not plenty of good reasons to change it, so be it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have no strong feelings about it, whatever you prefer

started_at: datetime | None = None
finished_at: datetime | None = None

if event_type == "JobCreatedEvent":

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.

what about JobUpdatedEvent ? For that one, these would be set to None here:

"created_at": created_at,
"started_at": started_at,
"finished_at": finished_at,

and later in the SQL, I see COALESCE and it would just keep the old values. I feel that it's functionally correct, but please add the comment at least here?

@kevinwallimann kevinwallimann Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The normal order of events is
1 ) JobCreatedEvent
2 ) JobStartedEvent
3 ) Zero or more JobUpdatedEvents
n ) JobFinishedEvent

Step 1 and 2 can alternatively be merged in one JobCreatedAndStartedEvent

  • created_at is defined as the timestamp when JobCreatedEvent or JobCreatedAndStartedEvent arrived
  • started_at is defined as the timestamp when JobStartedEvent or JobCreatedAndStartedEvent arrived
  • finished_at is defined as the timestamp when JobFinishedEvent arrived

For JobUpdatedEvent, there would be last_updated_at, but last_updated_at is used for all events, so there is no dedicated else block here for JobUpdatedEvent

If I would set created_at, started_at in the JobUpdatedEvent, it would overwrite the created_at from the JobCreatedEvent, which is not desirable.

Does that clarify?

Comment thread src/writers/sql/inserts.sql Outdated
finished_at = CASE
WHEN EXCLUDED.last_updated_at >= t.last_updated_at
THEN COALESCE(EXCLUDED.finished_at, t.finished_at)
ELSE t.finished_at

@lsulak lsulak Jul 23, 2026

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.

I'm thinking if this is not 'losing data' and more robust implementation can be like this:

ELSE COALESCE(t.finished_at, EXCLUDED.finished_at)

--

Imagine, that there are JobUpdatedEvent and JobFinishedEvent arrivals.

JobUpdatedEvent updates last_updated_at to value 100 (for simplicity here).

Then JobFinishedEvent arrives but has last_updated_at = 99 and that causes the finished_at to be NULL, because the condition EXCLUDED.last_updated_at >= t.last_updated_at will match the ELSE brach and that will keep the old record, which is NULL because the JobUpdatedEvent wouldn't carry this information about the job finish timestamp.

This is an edge case scenario, when the events arrive slightly out of order - the last_updated_at computed by producer has some redelivery/clock issiue. So out of order events would be corrupted by this, eventually leading to data loss (finished_at not being updated / present at all)

Btw, this aligns finished_at with the "take latest non-null" strategy declared in the ADR and with how created_at/started_at already behave.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, I will fix it, because it should indeed be the "take latest non-null" merge strategy according to the ADR.

I didn't cover the edge case you're describing. In the current implementation, a producer clock issue is out of scope for the out-of-order handling, as it relies on the timestamp to determine the true order.

The out-of-order handling is intended to account for reorderings introduced by the queue systems between the producer system and this Lambda, as they may not guarantee total order delivery.

Of course, it would be possible to determine the correct order by the assumed order of events, i.e. Created > Started > Updated > Finished, and for multiple Updated events, one could rely on the timestamp as a fallback.

Do you think we should cover that edge case?

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.

Aggregate status_change events and write to database table

2 participants