Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ This document outlines foundational mandates, architectural patterns, and projec
## 2. Time and Precision Handling (java.time Migration)

- **Idiomatic java.time Usage:** Avoid redundant conversions between `Instant` and `DateTime`. If a field or parameter is an `Instant`, use it directly. Do not convert to `DateTime` just to call a deprecated method if an `Instant` alternative exists or can be easily created. Furthermore, you should not call `toInstant()` or `toDateTime()` conversion methods when not strictly necessary; always prefer to use an alternative method that returns the correct type if one exists (e.g. use `tm().getTxTime()` which returns an `Instant` instead of calling `tm().getTransactionTime().toInstant()`).
- **CRITICAL MISTAKE TO AVOID:** NEVER use `toInstant(clock.nowUtc())` or `toInstant(fakeClock.nowUtc())`. Both `Clock` and `FakeClock` have a `now()` method that natively returns a `java.time.Instant`. You MUST use `clock.now()` or `fakeClock.now()` directly. Converting `nowUtc()` to an Instant is an embarrassing mistake that demonstrates a lack of basic codebase familiarity.
- **UTC Timezones:** Do not use `ZoneId.of("UTC")`. Use a statically imported `UTC` from `ZoneOffset` instead (`import static java.time.ZoneOffset.UTC;`).
- **Millisecond Precision:** Always truncate `Instant.now()` to milliseconds (using `.truncatedTo(ChronoUnit.MILLIS)`) to maintain consistency with Joda `DateTime` and the PostgreSQL schema (which enforces millisecond precision via JPA converters).
- **Clock Injection:**
- Avoid direct calls to `Instant.now()`, `DateTime.now()`, `ZonedDateTime.now()`, or `System.currentTimeMillis()`.
Expand Down Expand Up @@ -53,6 +55,10 @@ This document outlines foundational mandates, architectural patterns, and projec
### 6. Project Dependencies
- **Common Module:** When using `Clock` or other core utilities in a new or separate module (like `load-testing`), ensure `implementation project(':common')` is added to the module's `build.gradle`.

### 7. Search and Discovery
- **No CodeSearch:** This project is hosted on GitHub, not Google3. Do NOT use `mcp_Coding_search_for_files_codesearch` or other internal Google3 search tools.
- **Local Grep:** Use local shell commands like `git grep` or `grep` via `run_shell_command` to search the codebase.

## Performance and Efficiency
- **Turn Minimization:** Aim for "perfect" code in the first iteration. Iterative fixes for checkstyle or compilation errors consume significant context and time.
- **Context Management:** Use sub-agents for batch refactoring or high-volume output tasks to keep the main session history lean and efficient.
Expand Down
17 changes: 17 additions & 0 deletions common/src/main/java/google/registry/util/DateTimeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.time.format.DateTimeParseException;
import java.time.format.SignStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
Expand Down Expand Up @@ -252,6 +253,22 @@ public static org.joda.time.Instant toJodaInstant(@Nullable java.time.Instant in
return (instant == null) ? null : org.joda.time.Instant.ofEpochMilli(instant.toEpochMilli());
}

public static Instant plusHours(Instant instant, long hours) {
return instant.plus(hours, ChronoUnit.HOURS);
}

public static Instant minusHours(Instant instant, long hours) {
return instant.minus(hours, ChronoUnit.HOURS);
}

public static Instant plusMinutes(Instant instant, long minutes) {
return instant.plus(minutes, ChronoUnit.MINUTES);
}

public static Instant minusMinutes(Instant instant, long minutes) {
return instant.minus(minutes, ChronoUnit.MINUTES);
}

public static Instant plusDays(Instant instant, int days) {
return instant.atZone(ZoneOffset.UTC).plusDays(days).toInstant();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.RequestParameters.PARAM_DRY_RUN;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.END_INSTANT;
import static google.registry.util.RegistryEnvironment.PRODUCTION;

import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -99,7 +99,7 @@ public void run() {

private void deletePollMessages(String registrarId) {
ImmutableList<PollMessage> pollMessages =
PollFlowUtils.createPollMessageQuery(registrarId, END_OF_TIME).list();
PollFlowUtils.createPollMessageQuery(registrarId, END_INSTANT).list();
if (isDryRun) {
logger.atInfo().log(
"Would delete %d poll messages for registrar %s.", pollMessages.size(), registrarId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ private ImmutableList<Domain> processDomains(
tm().query(DOMAIN_QUERY_STRING, Domain.class)
.setParameter("tlds", deletableTlds)
.setParameter(
"creationTimeCutoff", CreateAutoTimestamp.create(now.minus(DOMAIN_USED_DURATION)))
"creationTimeCutoff",
CreateAutoTimestamp.create(toInstant(now.minus(DOMAIN_USED_DURATION))))
.setParameter("nowMinusSoftDeleteDelay", toInstant(now.minus(SOFT_DELETE_DELAY)))
.setParameter("now", toInstant(now));
ImmutableList<Domain> domainList =
Expand Down Expand Up @@ -304,12 +305,12 @@ private static void hardDeleteDomainsAndHosts(
// Take a DNS queue + admin registrar id as input so that it can be called from the mapper as well
private void softDeleteDomain(Domain domain) {
Domain deletedDomain =
domain.asBuilder().setDeletionTime(tm().getTransactionTime()).setStatusValues(null).build();
domain.asBuilder().setDeletionTime(tm().getTxTime()).setStatusValues(null).build();
DomainHistory historyEntry =
new DomainHistory.Builder()
.setDomain(domain)
.setType(DOMAIN_DELETE)
.setModificationTime(tm().getTransactionTime())
.setModificationTime(tm().getTxTime())
.setBySuperuser(true)
.setReason("Deletion of prober data")
.setRegistrarId(registryAdminRegistrarId)
Expand Down
37 changes: 21 additions & 16 deletions core/src/main/java/google/registry/beam/billing/BillingEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@

package google.registry.beam.billing;


import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import google.registry.reporting.billing.BillingModule;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Pattern;
import org.apache.beam.sdk.coders.AtomicCoder;
import org.apache.beam.sdk.coders.Coder;
Expand All @@ -29,9 +34,6 @@
import org.apache.beam.sdk.coders.VarIntCoder;
import org.apache.beam.sdk.coders.VarLongCoder;
import org.jetbrains.annotations.NotNull;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

/**
* A record representing a single billable event, parsed from a {@code SchemaAndRecord}.
Expand All @@ -53,8 +55,8 @@
*/
public record BillingEvent(
long id,
DateTime billingTime,
DateTime eventTime,
Instant billingTime,
Instant eventTime,
String registrarId,
String billingId,
String poNumber,
Expand All @@ -68,7 +70,7 @@ public record BillingEvent(
String flags) {

private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss zzz");
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss 'UTC'").withZone(ZoneOffset.UTC);

private static final Pattern SYNTHETIC_REGEX = Pattern.compile("SYNTHETIC", Pattern.LITERAL);

Expand All @@ -92,8 +94,8 @@ public record BillingEvent(
/** Creates a concrete {@link BillingEvent}. */
static BillingEvent create(
long id,
DateTime billingTime,
DateTime eventTime,
Instant billingTime,
Instant eventTime,
String registrarId,
String billingId,
String poNumber,
Expand Down Expand Up @@ -143,8 +145,8 @@ String toCsv() {
.join(
ImmutableList.of(
id(),
DATE_TIME_FORMATTER.print(billingTime()),
DATE_TIME_FORMATTER.print(eventTime()),
DATE_TIME_FORMATTER.format(billingTime()),
DATE_TIME_FORMATTER.format(eventTime()),
registrarId(),
billingId(),
poNumber(),
Expand All @@ -162,10 +164,13 @@ String toCsv() {
/** Returns the grouping key for this {@code BillingEvent}, to generate the overall invoice. */
InvoiceGroupingKey getInvoiceGroupingKey() {
return new InvoiceGroupingKey(
billingTime().toLocalDate().withDayOfMonth(1).toString(),
ZonedDateTime.ofInstant(billingTime(), ZoneOffset.UTC)
.toLocalDate()
.withDayOfMonth(1)
.toString(),
years() == 0
? ""
: billingTime()
: ZonedDateTime.ofInstant(billingTime(), ZoneOffset.UTC)
.toLocalDate()
.withDayOfMonth(1)
.plusYears(years())
Expand Down Expand Up @@ -308,8 +313,8 @@ private BillingEventCoder() {}
@Override
public void encode(BillingEvent value, OutputStream outStream) throws IOException {
longCoder.encode(value.id(), outStream);
stringCoder.encode(DATE_TIME_FORMATTER.print(value.billingTime()), outStream);
stringCoder.encode(DATE_TIME_FORMATTER.print(value.eventTime()), outStream);
stringCoder.encode(DATE_TIME_FORMATTER.format(value.billingTime()), outStream);
stringCoder.encode(DATE_TIME_FORMATTER.format(value.eventTime()), outStream);
stringCoder.encode(value.registrarId(), outStream);
stringCoder.encode(value.billingId(), outStream);
stringCoder.encode(value.poNumber(), outStream);
Expand All @@ -327,8 +332,8 @@ public void encode(BillingEvent value, OutputStream outStream) throws IOExceptio
public BillingEvent decode(InputStream inStream) throws IOException {
return new BillingEvent(
longCoder.decode(inStream),
DATE_TIME_FORMATTER.parseDateTime(stringCoder.decode(inStream)),
DATE_TIME_FORMATTER.parseDateTime(stringCoder.decode(inStream)),
Instant.from(DATE_TIME_FORMATTER.parse(stringCoder.decode(inStream))),
Instant.from(DATE_TIME_FORMATTER.parse(stringCoder.decode(inStream))),
stringCoder.decode(inStream),
stringCoder.decode(inStream),
stringCoder.decode(inStream),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,12 +276,12 @@ private void expandOneRecurrence(
ImmutableSet.copyOf(
billingRecurrence
.getRecurrenceTimeOfYear()
.getInstancesInRangeInstant(
.getInstancesInRange(
Range.closedOpen(
latestOf(
plusYears(billingRecurrence.getRecurrenceLastExpansionInstant(), 1),
plusYears(billingRecurrence.getRecurrenceLastExpansion(), 1),
startTime),
earliestOf(billingRecurrence.getRecurrenceEndTimeInstant(), endTime))));
earliestOf(billingRecurrence.getRecurrenceEndTime(), endTime))));
} catch (IllegalArgumentException e) {
return;
}
Expand All @@ -306,7 +306,7 @@ private void expandOneRecurrence(
return;
}

Instant recurrenceLastExpansionTime = billingRecurrence.getRecurrenceLastExpansionInstant();
Instant recurrenceLastExpansionTime = billingRecurrence.getRecurrenceLastExpansion();

// Create new OneTime and DomainHistory for EventTimes that needs to be expanded.
for (Instant eventTime : eventTimesToExpand) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ Optional<String> verifyDomainStillUnblockableWithReason(
}

boolean isStalenessAllowed(Domain domain) {
return domain.getCreationTimeInstant().plus(maxStaleness).isAfter(clock.now());
return domain.getCreationTime().plus(maxStaleness).isAfter(clock.now());
}

/** Returns unique labels across all block lists in the download specified by {@code jobName}. */
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/google/registry/flows/FlowModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ B makeHistoryEntryBuilder(
String registrarId,
EppInput eppInput) {
builder
.setModificationTime(tm().getTransactionTime())
.setModificationTime(tm().getTxTime())
.setTrid(trid)
.setXmlBytes(inputXmlBytes)
.setBySuperuser(isSuperuser)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ private DomainHistory buildDomainHistory(
ImmutableSet.of(
DomainTransactionRecord.create(
tld.getTldStr(),
now.plus(addGracePeriod),
toInstant(now.plus(addGracePeriod)),
TransactionReportField.netAddsFieldFromYears(period.getValue()),
1)));
}
Expand Down Expand Up @@ -607,13 +607,14 @@ private BillingEvent createBillingEvent(
.setRegistrarId(registrarId)
.setPeriodYears(years)
.setCost(feesAndCredits.getCreateCost())
.setEventTime(now)
.setEventTime(toInstant(now))
.setAllocationToken(allocationToken.map(AllocationToken::createVKey).orElse(null))
.setBillingTime(
now.plus(
isAnchorTenant
? tld.getAnchorTenantAddGracePeriodLength()
: tld.getAddGracePeriodLength()))
toInstant(
now.plus(
isAnchorTenant
? tld.getAnchorTenantAddGracePeriodLength()
: tld.getAddGracePeriodLength())))
.setFlags(flagsBuilder.build())
.setDomainHistoryId(domainHistoryId)
.build();
Expand All @@ -638,7 +639,7 @@ private BillingRecurrence createAutorenewBillingEvent(
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(targetId)
.setRegistrarId(registrarId)
.setEventTime(registrationExpirationTime)
.setEventTime(toInstant(registrationExpirationTime))
.setRecurrenceEndTime(END_INSTANT)
.setDomainHistoryId(domainHistoryId)
.setRenewalPriceBehavior(renewalPriceBehavior)
Expand All @@ -651,7 +652,7 @@ private Autorenew createAutorenewPollMessage(
return new PollMessage.Autorenew.Builder()
.setTargetId(targetId)
.setRegistrarId(registrarId)
.setEventTime(registrationExpirationTime)
.setEventTime(toInstant(registrationExpirationTime))
.setMsg("Domain was auto-renewed.")
.setDomainHistoryId(domainHistoryId)
.build();
Expand Down Expand Up @@ -685,7 +686,7 @@ private static PollMessage.OneTime createNameCollisionOneTimePollMessage(
String domainName, HistoryEntry historyEntry, String registrarId, DateTime now) {
return new PollMessage.OneTime.Builder()
.setRegistrarId(registrarId)
.setEventTime(now)
.setEventTime(toInstant(now))
.setMsg(COLLISION_MESSAGE) // Remind the registrar of the name collision policy.
.setResponseData(
ImmutableList.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.CollectionUtils.union;
import static google.registry.util.DateTimeUtils.toDateTime;
import static google.registry.util.DateTimeUtils.toInstant;

import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -162,7 +163,7 @@ public EppResponse run() throws EppException {
} else {
builder = existingDomain.asBuilder();
}
builder.setLastEppUpdateTime(now).setLastEppUpdateRegistrarId(registrarId);
builder.setLastEppUpdateTime(toInstant(now)).setLastEppUpdateRegistrarId(registrarId);
Duration redemptionGracePeriodLength = tld.getRedemptionGracePeriodLength();
Duration pendingDeleteLength = tld.getPendingDeleteLength();
Optional<DomainDeleteSuperuserExtension> domainDeleteSuperuserExtension =
Expand All @@ -187,13 +188,13 @@ public EppResponse run() throws EppException {
historyBuilder.setRevisionId(domainHistoryId.getRevisionId());
DateTime deletionTime = now.plus(durationUntilDelete);
if (durationUntilDelete.equals(Duration.ZERO)) {
builder.setDeletionTime(now).setStatusValues(null);
builder.setDeletionTime(toInstant(now)).setStatusValues(null);
} else {
DateTime redemptionTime = now.plus(redemptionGracePeriodLength);
asyncTaskEnqueuer.enqueueAsyncResave(
existingDomain.createVKey(), now, ImmutableSortedSet.of(redemptionTime, deletionTime));
builder
.setDeletionTime(deletionTime)
.setDeletionTime(toInstant(deletionTime))
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
// Clear out all old grace periods and add REDEMPTION, which does not include a key to a
// billing event because there isn't one for a domain delete.
Expand Down Expand Up @@ -242,7 +243,8 @@ public EppResponse run() throws EppException {
// No cancellation is written if the grace period was not for a billable event.
if (gracePeriod.hasBillingEvent()) {
entitiesToInsert.add(
BillingCancellation.forGracePeriod(gracePeriod, now, domainHistoryId, targetId));
BillingCancellation.forGracePeriod(
gracePeriod, toInstant(now), domainHistoryId, targetId));
if (gracePeriod.getBillingEvent() != null) {
// Take the amount of registration time being refunded off the expiration time.
// This can be either add grace periods or renew grace periods.
Expand Down Expand Up @@ -290,7 +292,7 @@ public EppResponse run() throws EppException {
flowCustomLogic.beforeResponse(
BeforeResponseParameters.newBuilder()
.setResultCode(
newDomain.getDeletionDateTime().isAfter(now)
newDomain.getDeletionTime().isAfter(toInstant(now))
? SUCCESS_WITH_ACTION_PENDING
: SUCCESS)
.setResponseExtensions(
Expand Down Expand Up @@ -343,7 +345,7 @@ private DomainHistory buildDomainHistory(
cancelledRecords,
DomainTransactionRecord.create(
domain.getTld(),
now.plus(durationUntilDelete),
toInstant(now.plus(durationUntilDelete)),
inAddGracePeriod
? TransactionReportField.DELETED_DOMAINS_GRACE
: TransactionReportField.DELETED_DOMAINS_NOGRACE,
Expand Down Expand Up @@ -380,7 +382,7 @@ private PollMessage.OneTime createImmediateDeletePollMessage(
Domain existingDomain, HistoryEntryId domainHistoryId, DateTime now, DateTime deletionTime) {
return new PollMessage.OneTime.Builder()
.setRegistrarId(existingDomain.getPersistedCurrentSponsorRegistrarId())
.setEventTime(now)
.setEventTime(toInstant(now))
.setDomainHistoryId(domainHistoryId)
.setMsg(
String.format(
Expand Down Expand Up @@ -421,7 +423,10 @@ private Money getGracePeriodCost(
if (gracePeriod.getType() == GracePeriodStatus.AUTO_RENEW) {
// If we updated the autorenew billing event, reuse it.
DateTime autoRenewTime =
billingRecurrence.getRecurrenceTimeOfYear().getLastInstanceBeforeOrAt(now);
toDateTime(
billingRecurrence
.getRecurrenceTimeOfYear()
.getLastInstanceBeforeOrAt(toInstant(now)));
return getDomainRenewCost(targetId, toInstant(autoRenewTime), 1);
}
return tm().loadByKey(checkNotNull(gracePeriod.getBillingEvent())).getCost();
Expand Down
Loading
Loading