finished_at and archived_at are stored with Go's monotonic clock reading appended (m=+365.03) #529

Closed
opened 2026-10-13 11:32:25 +02:00 by clawbot · 2 comments
Collaborator

Severity: medium

What is wrong. Two timestamps are assigned from a bare time.Now() and handed to the driver, which stores a time as the text of time.Time.String(): writeDeliveryStatus in internal/delivery/engine.go ("finished_at": time.Now()) and archiveWriter.write in internal/delivery/target_database_archive.go (row.ArchivedAt = time.Now()). A time.Now() carries the monotonic clock reading and String() appends it, so the stored text is of the form 2026-10-13 09:07:37.840204068 +0000 UTC m=+365.027775927. Every other timestamp (GORM's created_at and updated_at, the totals' last_event_at) is stored without the suffix. The driver (modernc.org/sqlite, parseTimeString) strips the suffix on read, so the service reads its own rows back correctly and the gzipped JSON export is clean.

How it was observed. On the tag's image, select finished_at from deliveries and select archived_at from archived_events on the files under DATA_DIR show the m=+… suffix on every row; created_at beside them has none.

Why it matters. The archive file is documented as long-term storage that an operator keeps, moves away for offline retention and reads with other tools; its archived_at column carries a number that means nothing outside the process that wrote it (seconds since that process started). After 1.0 the stored format is a compatibility surface (REPO_POLICIES.md: migrations are never edited after release), so this is cheap to fix before the tag and awkward after. The SQL comparisons on these columns (archived_at < ? in the archive prune, finished_at >= ? in the statistics) still work because the bound parameter is formatted the same way and the date part decides, so no misbehaviour was observed.

What acceptable looks like. Strip the monotonic reading before storing (time.Now().Round(0) or .UTC()), ideally one consistent form for every stored timestamp. Existing rows need no migration before 1.0.

Definition of done. New finished_at and archived_at values are stored without the m= suffix, in the same form as created_at; a test asserts the stored text; the archive export is unchanged.

Model: fable-5-1

Severity: medium **What is wrong.** Two timestamps are assigned from a bare `time.Now()` and handed to the driver, which stores a time as the text of `time.Time.String()`: `writeDeliveryStatus` in `internal/delivery/engine.go` (`"finished_at": time.Now()`) and `archiveWriter.write` in `internal/delivery/target_database_archive.go` (`row.ArchivedAt = time.Now()`). A `time.Now()` carries the monotonic clock reading and `String()` appends it, so the stored text is of the form `2026-10-13 09:07:37.840204068 +0000 UTC m=+365.027775927`. Every other timestamp (GORM's `created_at` and `updated_at`, the totals' `last_event_at`) is stored without the suffix. The driver (`modernc.org/sqlite`, `parseTimeString`) strips the suffix on read, so the service reads its own rows back correctly and the gzipped JSON export is clean. **How it was observed.** On the tag's image, `select finished_at from deliveries` and `select archived_at from archived_events` on the files under `DATA_DIR` show the `m=+…` suffix on every row; `created_at` beside them has none. **Why it matters.** The archive file is documented as long-term storage that an operator keeps, moves away for offline retention and reads with other tools; its `archived_at` column carries a number that means nothing outside the process that wrote it (seconds since that process started). After 1.0 the stored format is a compatibility surface (`REPO_POLICIES.md`: migrations are never edited after release), so this is cheap to fix before the tag and awkward after. The SQL comparisons on these columns (`archived_at < ?` in the archive prune, `finished_at >= ?` in the statistics) still work because the bound parameter is formatted the same way and the date part decides, so no misbehaviour was observed. **What acceptable looks like.** Strip the monotonic reading before storing (`time.Now().Round(0)` or `.UTC()`), ideally one consistent form for every stored timestamp. Existing rows need no migration before 1.0. **Definition of done.** New `finished_at` and `archived_at` values are stored without the `m=` suffix, in the same form as `created_at`; a test asserts the stored text; the archive export is unchanged. Model: fable-5-1
clawbot self-assigned this 2026-10-13 11:32:25 +02:00
Author
Collaborator

Plan: store every timestamp the delivery package writes without the monotonic reading, in the same form GORM writes created_at.

  • writeDeliveryStatus in internal/delivery/engine.go and archiveWriter.write in internal/delivery/target_database_archive.go: use the same clock GORM uses for created_at (check NowFunc in the database setup; if it is GORM's default, time.Now().Local()), so the stored text matches. Grep the package for any other bare time.Now() handed to the database and fix it the same way.
  • Test in internal/delivery: read the raw stored text of finished_at and archived_at with a SQL query that casts to text, and assert there is no m= suffix and that it has the same layout as created_at. The gzipped JSON export stays byte-for-byte as it is.

Model: opus-5-5

Plan: store every timestamp the delivery package writes without the monotonic reading, in the same form GORM writes `created_at`. - `writeDeliveryStatus` in `internal/delivery/engine.go` and `archiveWriter.write` in `internal/delivery/target_database_archive.go`: use the same clock GORM uses for `created_at` (check `NowFunc` in the database setup; if it is GORM's default, `time.Now().Local()`), so the stored text matches. Grep the package for any other bare `time.Now()` handed to the database and fix it the same way. - Test in `internal/delivery`: read the raw stored text of `finished_at` and `archived_at` with a SQL query that casts to text, and assert there is no ` m=` suffix and that it has the same layout as `created_at`. The gzipped JSON export stays byte-for-byte as it is. Model: opus-5-5
Author
Collaborator

Implemented in #533. finished_at, archived_at and the updated_at stamp written when a stranded delivery is sent again (the issue's "every other timestamp" missed that one) now come from GORM's clock, the one that stamps created_at, so they are stored in the same form. Tests read the stored text back.

Model: opus-5-5

Implemented in https://git.eeqj.de/sneak/webhooker/pulls/533. `finished_at`, `archived_at` and the `updated_at` stamp written when a stranded delivery is sent again (the issue's "every other timestamp" missed that one) now come from GORM's clock, the one that stamps `created_at`, so they are stored in the same form. Tests read the stored text back. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#529