Repository navigation
Conversation
|
Hi @innoprej, We greatly appreciate your contribution and the effort you put into this pull request. It is currently under review by our team. We will notify you if any additional details are needed. |
| if (config.afterTimestamp().isPresent()) { | ||
| // Round up to whole milliseconds, the precision of stored event timestamps. | ||
| Instant cursor = | ||
| config.afterTimestamp().get().plusNanos(999_999).truncatedTo(ChronoUnit.MILLIS); |
There was a problem hiding this comment.
plusNanos(999_999) overflows near Instant.MAX: an afterTimestamp of Instant.MAX throws DateTimeException instead of returning no events like InMemorySessionService. This is an edge case, but the overflow is easy to guard against.
There was a problem hiding this comment.
Thanks for catching this. I added a guard for cursors later than the maximum event timestamp representable as a long epoch-millisecond value. In that case, getSession returns the session with no events without issuing the event query. The new tests cover both Instant.MAX and a cursor one nanosecond beyond the maximum event timestamp.
| .thenReturn(mockQuery); | ||
| when(mockQuery.get()).thenReturn(ApiFutures.immediateFuture(mockQuerySnapshot)); | ||
|
|
||
| storedCursorByAfterTimestamp.forEach( |
There was a problem hiding this comment.
Running three cases in one forEach lets the first failure hide the other two and forces one test name to cover three behaviors. Splitting this into three small tests (whole second, millisecond, sub-millisecond rounded up) would isolate each case. Also, each getSession(...).test() here fails silently because the stubbed session data lacks updateTime, so Instant.parse throws. Stubbing the full session map and calling assertComplete() would test the read path too, not just the query.
There was a problem hiding this comment.
Thanks for pointing out the silent failure. I split the whole-second, millisecond, and sub-millisecond cases into separate tests, supplied the complete session data including updateTime, and added assertions that the read completes without errors and returns the expected session.
| private static final String UPDATE_TIME_KEY = Constants.KEY_UPDATE_TIME; | ||
| private static final String TIMESTAMP_KEY = Constants.KEY_TIMESTAMP; | ||
|
|
||
| /** Text, not a timestamp as in ADK Python, with three fraction digits so it sorts by time. */ |
There was a problem hiding this comment.
LOW: The summary does not state what this constant is or why the field stays text. Something like "Formats stored event timestamps with exactly three fraction digits, so text order is time order" works, followed by the reason from your commit message: Firestore orders timestamps before strings, so switching types would misorder existing sessions.
There was a problem hiding this comment.
Thanks, I updated the Javadoc to describe the fixed three-digit fractional format and explain why the field remains text for compatibility with existing events.
FirestoreSessionService stored event timestamps with Instant.toString(), which leaves out the fraction when it is zero, and ordered and filtered events by comparing those strings. Within one second, an event at .000 sorted after the later events, a whole-second afterTimestamp cursor left out the rest of that second, and afterTimestamp used > where InMemorySessionService, VertexAiSessionService and ADK Python keep events at or after the cursor. - Event timestamps are written with exactly three fraction digits. - afterTimestamp is rounded up to whole milliseconds, written the same way and matched with >=. - An afterTimestamp later than any event can have (event timestamps are epoch milliseconds in a long) returns no events, as InMemorySessionService does, instead of overflowing when rounded up near Instant.MAX. - getSession and listEvents sort the events they read by timestamp, so events stored in the old format also come back in time order. The field stays a string. ADK Python stores a Firestore timestamp, but Firestore orders timestamps before strings, so sessions with events stored before this change would be misordered.
153a150 to
1a6dd29
Compare
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
2. Or, if no issue exists, describe the change:
Problem:
FirestoreSessionServicestores each event'stimestampasInstant.toString(), which leaves out the fraction when it is zero, and orders and filters events by comparing those strings. Within one second, an event at.000(…05Z) sorts after the later events (…05.400Z), sogetSessionandlistEventsreturn them out of order.afterTimestampis compared as text too and with>: a whole-second cursor leaves out the rest of that second, a millisecond cursor includes an earlier whole-second event, and an event at exactly the cursor is left out, whileInMemorySessionService,VertexAiSessionServiceand ADK Python keep the events at or after the cursor. #1646 has the details and a reproduction.Solution:
eventToMapwritestimestampwith exactly three fraction digits (DateTimeFormatterBuilder.appendInstant(3), for example2026-10-10T05:00:05.000Z). Event timestamps are whole milliseconds, so the digits are exact and text order is time order.eventFromMapandFirestoreMemoryServiceread both forms, as before.getSessionroundsafterTimestampup to whole milliseconds, writes it the same way and filters withwhereGreaterThanOrEqualTo. An event at millisecondmis at or after the cursortexactly whenm >= ceil(t), which is whatInMemorySessionServicekeeps. The query still orders and filters on the one field, so no new index is needed.afterTimestampis later than the largest timestamp an event can have(
Long.MAX_VALUEepoch milliseconds),getSessionreturns the sessionwith no events without issuing the event query. This also avoids overflow
when rounding a cursor near
Instant.MAX.getSessionandlistEventssort the events they read by timestamp, asVertexAiSessionService.filterEventsdoes. Events stored before this change keep their old strings, and this keeps those sessions in time order too.Testing Plan
Unit Tests:
On Windows 11 with Microsoft Build of OpenJDK 17.0.19,
./mvnw -pl contrib/firestore-session-service testran 60 tests with twofailures, both in
FirestorePropertiesTest:shouldReturnHardcodedDefaultIfPropertyFileHasNoEntryandshouldLoadDevPropertiesWhenEnvIsSetToDev. The same two tests failed withthe same messages in the 55-test baseline run on the previous PR head
153a150a. The focusedFirestoreSessionServiceTestrun passed all 37 tests.On the updated head
1a6dd29e, GitHub Actions Scan and CLA checks passed.validation,PR Title Check, andPR Commit Checkare awaiting approvalto run (
action_required).mainappendEvent_storesTimestampsWithThreeFractionDigits: events at05.000,05.400and06.000…05Z,…05.400Z,…06ZmaingetSession_withAfterTimestampAtInstantMax_returnsSessionWithoutEventsmain; fails withDateTimeExceptionon previous PR head153a150agetSession_eventsStoredOutOfOrder_returnsEventsInTimeOrder: documents…05.400Zand…05Z, in Firestore's text order05.400firstlistEvents_eventsStoredOutOfOrder_returnsEventsInTimeOrder: the same forlistEvents05.400firstIn the earlier test of PR head
153a150a, rounding the cursor down insteadof up caused the sub-millisecond case to fail (
…05.200Zinstead of…05.201Z).Manual End-to-End (E2E) Tests:
Not run against a real Firestore database or the emulator. The tests check the strings and the query the service sends to a mocked
Firestore; the order and the result sets then follow from Firestore's documented ordering of strings by UTF-8 bytes. In an earlier local run on PR head153a150awith #1643 and #1650 appliedtogether, all 38 tests in
FirestoreSessionServiceTestpassed. That combinedtree was not rerun with the current review changes.
Checklist
Additional context
numRecentEventscan return an old…05Zevent instead of a later…05.400Zone, andafterTimestampcan include an old…05Zevent although the cursor is later in that second. It no longer leaves out events at or after the cursor.timestampstays a string, a deliberate difference from ADK Python noted in theTIMESTAMP_FORMATTERJavadoc. ADK Python stores a Firestore timestamp, but Firestore sorts timestamps before strings, so sessions holding both would be misordered unless the stored events are migrated. If you would prefer the timestamp type with a migration, I can change this.FirestoreMemoryServicereturns the stored string asMemoryEntry.timestamp, so entries for events at a whole second now read…05.000Zinstead of…05Z. Both are ISO 8601.updateTimeis still written withtoString(); it is only parsed back, never compared in a query.