unbridled-41 opened a new pull request, #4949:
URL: https://github.com/apache/rocketmq-dashboard/pull/4949
## Problem
Two tables expose a bookkeeping column that the database, not the
application, writes — while every other timestamp of the same row is written in
UTC and read as UTC by the console. On a database whose time zone is not UTC
the shown value is offset from the rest of its own row, and the alert-delivery
retention sweep runs up to one zone offset away from the configured window.
## Evidence
Base commit `1ef5d860`.
**Alert notification deliveries**
* `server/src/main/resources/db/schema.sql:375-376` (base) —
`gmt_create`/`gmt_modified` are `` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
[ON UPDATE CURRENT_TIMESTAMP] ``.
* The entity declares no `gmtCreate` at all
(`RmqAlertNotificationOutbox.java:24-30` base), and `grep -rn setGmtCreate
server/src/main/java` finds no writer for this table, so MyBatis-Plus's
`insert` never includes the column: the default applies.
* The same row's other timestamps come from `utcNow()` —
`NotificationOutboxService.java:192` (`next_attempt_at`), `:367`
(`delivered_at`) — and the cleanup cutoff is `utcNow().minus(retention)` at
`:321`.
* But the retention predicate compares that UTC cutoff against the DB-clock
column: `RmqAlertNotificationOutboxMapper.java:28-29` (base) — `OR
(delivered_at IS NULL AND gmt_modified < #{cutoff})) OR (status = 'FAILED' AND
gmt_modified < #{cutoff})`.
* The console renders `createdAt` as UTC:
`web/src/pages/ops/notificationDeliveries.tsx:212` (base)
`formatUtcDateTime(record.deliveredAt ?? record.createdAt)` and `:348` for the
drawer's "Created At"; `web/src/utils/format.ts:49-58` appends `Z` to a naive
string. A PENDING row has no `deliveredAt`, so its creation time is what the
page shows.
* Shipped deployment: `deploy/docker-compose.yml:13` sets the MySQL
container `TZ: ${TZ:-Asia/Shanghai}` and the app's JDBC URL (`:86`) uses
`serverTimezone=Asia/Shanghai`, so `CURRENT_TIMESTAMP` is evaluated in
Asia/Shanghai while `next_attempt_at`/`delivered_at` are stored as UTC.
**User-management sessions**
* `schema.sql:32-33` (base) — same defaults for `rmq_studio_session`.
* `AuthService.createSession` (`:393-398` base) sets `user_id`,
`token_hash`, `last_seen_at` and `expires_at` (from `now()` =
`LocalDateTime.ofInstant(Instant.ofEpochMilli(clock.millis()), ZoneOffset.UTC)`
at `:620-621`) and leaves `gmt_create` unset.
* The session list selects the three together — `AuthService.java:283`
(base) `.select("id", "user_id", "last_seen_at", "expires_at", "gmt_create")` —
and `web/src/pages/studio/UserManagement.tsx` renders `lastSeenAt`, `expiresAt`
and `gmtCreate` through the same `formatUtcDateTime` column renderer.
## Root cause
The application's convention for these tables is "zone-less columns hold
UTC". Both columns are exceptions that no Java code writes, so the MySQL
default supplies the session's clock instead, and one row ends up reading two
different time bases.
## Fix
Write the columns from the clock that already writes the rest of the row:
* `RmqAlertNotificationOutbox` gains `gmtCreate`; `enqueue` stamps both
columns via a small `stampBookkeepingColumns` helper; every mutation that
already has a `now` stamps `gmt_modified` with it (`retryFailedDelivery`, the
delivered transition, `retry`, `deferUntilSilenceEnds`); the two raw statements
`claimForDispatch` and `renewClaim` set it in SQL, because they bypass
MyBatis-Plus's set clause.
* `AuthService.createSession` sets `session.setGmtCreate(current)`.
* `rmq_studio_session.gmt_modified` is deliberately **not** stamped: nothing
reads it for that table. `schema.sql` is untouched — the defaults remain as a
fallback for rows written by anything outside the application, and no migration
is needed.
## Scoring (AGENTS.md)
* PRIORITY **62** = impact 20 (operator-visible creation times wrong by the
database's offset on two admin surfaces; a terminal delivery retained for
retention ± one zone offset) + reach 16 (deliveries page, deliveries drawer,
user-management session list, the cleanup job) + reproducibility 14
(deterministic whenever the database zone is not UTC, which the shipped compose
file arranges) + maintenance value 12 (systemic convention, not a one-off).
* FIX_CONFIDENCE **75** — the direction is not in doubt (one row, one
clock); it is below 80 because the alternative — moving the two read paths onto
the database's clock — is a defensible product choice for other deployments.
## Tests
New tests in `NotificationOutboxServiceTest` and `AuthServiceDatabaseTest`.
Pre-fix, with the tests kept and only the fixed production files restored to
`1ef5d860`:
```
NotificationOutboxServiceTest.mutationsShouldStampGmtModifiedInsteadOfLeavingItToTheColumnDefaultTest
Expecting actual:
"status=#{...},attempt_count=#{...},next_attempt_at=#{...},sending_started_at=#{...},claim_token=#{...},last_error=#{...}"
to contain:
"gmt_modified="
NotificationOutboxServiceTest.claimedAndRenewedRowsShouldStampGmtModifiedInTheStatementItselfTest
[the SQL of claimForDispatch]
Expecting actual:
"UPDATE rmq_alert_notification_outbox SET status = 'SENDING',
sending_started_at = #{claimedAt}, claim_token = #{claimToken} WHERE ..."
to contain:
"gmt_modified ="
AuthServiceDatabaseTest.databaseLoginStampsTheSessionCreationTimeInUtc
expected: 2026-08-13T00:00 (java.time.LocalDateTime)
but was: null
NotificationOutboxServiceTest.java:[250,51] cannot find symbol: method
getGmtCreate()
location: class RmqAlertNotificationOutbox
```
The last one is the honest shape of the insert half: before the fix the
entity has no `gmtCreate` property, so the column *cannot* be written from Java
and the schema default is the only writer. That is why the pre-fix evidence for
it is a compile error plus the schema line rather than a failing assertion.
Post-fix, from the branch:
```
mvn -o test
-Dtest='NotificationOutboxServiceTest,AlertSilenceServiceTest,MybatisPlusAlertRepositoryTest,SystemAlertControllerTest,AlertServiceTest,SuppressionServiceTest,NativeAlertProcessorTest'
NotificationOutboxServiceTest 29/29, AlertServiceTest 83/83,
SystemAlertControllerTest 11/11,
NativeAlertProcessorTest 23/23, AlertSilenceServiceTest 12/12,
MybatisPlusAlertRepositoryTest 16/16, ...
mvn -o test
-Dtest='AuthServiceDatabaseTest,AuthServiceTest,AuthControllerTest'
AuthServiceDatabaseTest 30/30, AuthServiceTest 16/16
```
## Risk
Low. `gmt_modified` is now written on the same statements that already
rewrote other columns of the row, so the "row last changed" semantics are
unchanged; only the clock behind it is. Rows written before the upgrade keep
their database-clock values, which is what any retention change would have to
deal with anyway.
## Boundary
Only the two surfaces where a database-clock column sits next to UTC columns
of the same row and is rendered as UTC are in scope. Other entities either set
`gmt_create` from Java already (`AiEventSink`, `AiRunService`,
`OperationAuditService`, `MybatisPlusSettingsRepository`) or expose no
UTC-rendered sibling column on the same read path.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]