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]

Reply via email to