feat: enable microsecond timestamps in client - #14057
Conversation
kanchi12P
commented
Aug 12, 2026
- Enable microsecond timestamp flag in stub settings
- Plumb timestamp origin to Mutation setCell
- Skip emulator in mutation-based integration tests to prevent failures
This pulls in the proto changes for microsecond timestamps from google3 and generates the corresponding Java classes in proto-google-cloud-bigtable-v2. Change-Id: I32650601b901f6e64a106e24a280a2ee608ce5e6
- Enable microsecond timestamp flag in stub settings - Plumb timestamp origin to Mutation setCell - Skip emulator in mutation-based integration tests to prevent failures Change-Id: Ifd8824a5934f17c9d6a5e0e8cf8b0a0a7343ccb1
There was a problem hiding this comment.
Code Review
This pull request introduces microsecond timestamp granularity for Bigtable mutations, including setting the timestamp origin (client auto-generated vs. user-specified) and enabling the microsecond timestamp feature flag in stub settings. Integration tests are updated to skip emulator environments that do not support this granularity, and several protobuf-generated files are regenerated. The review feedback suggests avoiding a potential JDK overflow bug in the year 2262 by directly calculating microseconds since epoch instead of using Instant.until, and recommends adding a @Nonnull annotation to the timestampOrigin parameter for consistency.
| public Mutation setCell( | ||
| @Nonnull String familyName, @Nonnull ByteString qualifier, @Nonnull ByteString value) { | ||
| long timestamp = System.currentTimeMillis() * 1_000; | ||
| long timestamp = Instant.EPOCH.until(Instant.now(), ChronoUnit.MICROS); |
There was a problem hiding this comment.
Using Instant.EPOCH.until(Instant.now(), ChronoUnit.MICROS) has a subtle but critical drawback in Java: the JDK implementation of Instant.until(..., ChronoUnit.MICROS) internally calculates the difference in nanoseconds first (nanosUntil) and then divides by 1000. Because nanoseconds since epoch overflow a long in the year 2262, this call will throw an ArithmeticException after that date, even though microseconds since epoch can easily fit in a long for another 292,000 years.
Additionally, it introduces unnecessary overhead due to temporal unit dispatching and multiple overflow-checked math operations.
We can compute the microseconds since epoch directly and safely using:
Instant now = Instant.now();
long timestamp = now.getEpochSecond() * 1_000_000L + now.getNano() / 1_000;This is more efficient, avoids the JDK overflow bug, and is extremely clean.
| long timestamp = Instant.EPOCH.until(Instant.now(), ChronoUnit.MICROS); | |
| Instant now = Instant.now(); | |
| long timestamp = now.getEpochSecond() * 1_000_000L + now.getNano() / 1_000; |
| private Mutation setCell( | ||
| @Nonnull String familyName, | ||
| @Nonnull ByteString qualifier, | ||
| long timestamp, | ||
| @Nonnull ByteString value, | ||
| TimestampOrigin timestampOrigin) { |
There was a problem hiding this comment.
The timestampOrigin parameter is not annotated with @Nonnull, whereas other object parameters in this method (and other overloads) are. Adding @Nonnull improves type safety and consistency.
| private Mutation setCell( | |
| @Nonnull String familyName, | |
| @Nonnull ByteString qualifier, | |
| long timestamp, | |
| @Nonnull ByteString value, | |
| TimestampOrigin timestampOrigin) { | |
| private Mutation setCell( | |
| @Nonnull String familyName, | |
| @Nonnull ByteString qualifier, | |
| long timestamp, | |
| @Nonnull ByteString value, | |
| @Nonnull TimestampOrigin timestampOrigin) { |
| @Test | ||
| public void setCellTest() { | ||
| long minTimestamp = System.currentTimeMillis() * 1_000; | ||
| long minTimestamp = Instant.EPOCH.until(Instant.now(), ChronoUnit.MICROS); |
There was a problem hiding this comment.
To avoid the JDK overflow bug in Instant.until(..., ChronoUnit.MICROS) (which throws ArithmeticException after the year 2262 due to internal nanosecond calculations) and to improve performance, we can compute the microseconds since epoch directly.
| long minTimestamp = Instant.EPOCH.until(Instant.now(), ChronoUnit.MICROS); | |
| Instant minInstant = Instant.now(); | |
| long minTimestamp = minInstant.getEpochSecond() * 1_000_000L + minInstant.getNano() / 1_000; |
| List<com.google.bigtable.v2.Mutation> actual = mutation.getMutations(); | ||
|
|
||
| long maxTimestamp = System.currentTimeMillis() * 1_000; | ||
| long maxTimestamp = Instant.EPOCH.until(Instant.now(), ChronoUnit.MICROS); |
There was a problem hiding this comment.
To avoid the JDK overflow bug in Instant.until(..., ChronoUnit.MICROS) (which throws ArithmeticException after the year 2262 due to internal nanosecond calculations) and to improve performance, we can compute the microseconds since epoch directly.
| long maxTimestamp = Instant.EPOCH.until(Instant.now(), ChronoUnit.MICROS); | |
| Instant maxInstant = Instant.now(); | |
| long maxTimestamp = maxInstant.getEpochSecond() * 1_000_000L + maxInstant.getNano() / 1_000; |
fe7343e to
41eff3e
Compare
|
@mutianf Please review |