Compare commits

..
Author SHA1 Message Date
gbrodmanandGitHub 3474cd6e9b Batch DNS refresh requests on host renames (#3181)
Some hosts can have more than 100k domains linked to them so we probably
don't want to insert all those entries at once.
2026-08-04 02:36:50 +00:00
gbrodmanandGitHub 1fc4a281c0 Add an IncrementalMetric for sync-cache-action runs (#3195)
This is configured to run every 5 minutes. We need to make sure that the
cache doesn't get too out of date, otherwise we'll be serving stale
data. We'll add an alert that fires if SUCCESS or NOT_CONFIGURED hasn't
happened recently.
2026-08-03 20:17:12 +00:00
Juan CelhayandGitHub 9a420a69b0 Add pre and post deploy steps to Cloud Deploy delivery pipeline (#3187)
* Fix image replacement in cd (#3186)

* read sql jobs from ar

* revert release change

* flatten file path for sql jobs

* no source to sql command

* add automation to pipeline

* fix automation

* fix replica seize for backend and console in partial phases
2026-08-03 18:34:47 +00:00
Pavlo TkachandGitHub a3421f2999 Update IPs name to match reserved for epp-server (#3194) 2026-08-03 13:42:58 +00:00
Pavlo TkachandGitHub 72c610688a Remove canary release build for epp server (#3193) 2026-08-01 02:02:07 +00:00
10 changed files with 170 additions and 73 deletions
@@ -28,6 +28,9 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.monitoring.metrics.IncrementableMetric;
import com.google.monitoring.metrics.LabelDescriptor;
import com.google.monitoring.metrics.MetricRegistryImpl;
import google.registry.cache.SimplifiedJedisClient;
import google.registry.model.EppResource;
import google.registry.model.common.Cursor;
@@ -39,6 +42,7 @@ import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import google.registry.util.NonFinalForTesting;
import jakarta.inject.Inject;
import java.time.Duration;
import java.time.Instant;
@@ -61,6 +65,25 @@ public class SyncRemoteCacheAction implements Runnable {
private static final String LOCK_NAME = "syncRemoteCacheAction";
private static final int BATCH_SIZE = 10000;
public enum SyncStatus {
SUCCESS,
FAILURE,
NOT_CONFIGURED
}
private static final ImmutableSet<LabelDescriptor> LABEL_DESCRIPTORS =
ImmutableSet.of(
LabelDescriptor.create("status", "Whether SyncRemoteCacheAction succeeded or failed."));
@NonFinalForTesting
static final IncrementableMetric SYNC_CACHE_RUNS_METRIC =
MetricRegistryImpl.getDefault()
.newIncrementableMetric(
"/batch/sync_remote_cache/runs",
"Count of SyncRemoteCacheAction executions",
"count",
LABEL_DESCRIPTORS);
private final LockHandler lockHandler;
private final Response response;
private final Optional<SimplifiedJedisClient> jedisClient;
@@ -79,14 +102,17 @@ public class SyncRemoteCacheAction implements Runnable {
if (jedisClient.isEmpty()) {
response.setStatus(SC_NO_CONTENT);
response.setPayload("No Jedis/Valkey configuration found");
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.NOT_CONFIGURED.name());
return;
}
Callable<Void> runner =
() -> {
try {
runLocked();
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.SUCCESS.name());
response.setStatus(SC_OK);
} catch (Exception e) {
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.FAILURE.name());
logger.atSevere().withCause(e).log("Errored out during execution.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload(String.format("Errored out with cause: %s", e));
@@ -95,6 +121,7 @@ public class SyncRemoteCacheAction implements Runnable {
};
if (!lockHandler.executeWithLocks(runner, null, Duration.ofHours(1), LOCK_NAME)) {
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.FAILURE.name());
// Send a 200-series status code to prevent this conflicting action from retrying.
response.setStatus(SC_NO_CONTENT);
response.setPayload("Could not acquire lock; already running?");
@@ -14,14 +14,18 @@
package google.registry.dns;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
import static google.registry.dns.RefreshDnsOnHostRenameAction.PATH;
import static google.registry.model.EppResourceUtils.getLinkedDomainKeys;
import static google.registry.model.EppResourceUtils.isDeleted;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.net.MediaType;
import google.registry.model.EppResourceUtils;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
@@ -29,8 +33,11 @@ import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
@Action(
service = Action.Service.BACKEND,
@@ -43,45 +50,52 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
public static final String PARAM_HOST_KEY = "hostKey";
public static final String PATH = "/_dr/task/refreshDnsOnHostRename";
private static final int DNS_REFRESH_BATCH_SIZE = 1000;
private final VKey<Host> hostKey;
private final Response response;
private final Clock clock;
@Inject
RefreshDnsOnHostRenameAction(@Parameter(PARAM_HOST_KEY) String hostKey, Response response) {
RefreshDnsOnHostRenameAction(
@Parameter(PARAM_HOST_KEY) String hostKey, Response response, Clock clock) {
this.hostKey = VKey.createEppVKeyFromString(hostKey);
this.response = response;
this.clock = clock;
}
@Override
public void run() {
tm().transact(
() -> {
Instant now = tm().getTxTime();
Host host = tm().loadByKeyIfPresent(hostKey).orElse(null);
boolean hostValid = true;
String failureMessage = null;
if (host == null) {
hostValid = false;
failureMessage = String.format("Host to refresh does not exist: %s", hostKey);
} else if (EppResourceUtils.isDeleted(host, now)) {
hostValid = false;
failureMessage =
String.format("Host to refresh is already deleted: %s", host.getHostName());
} else {
getLinkedDomainKeys(
host.createVKey(), host.getUpdateTimestamp().getTimestamp(), null)
.stream()
.map(domainKey -> tm().loadByKey(domainKey))
.filter(Domain::shouldPublishToDns)
.forEach(domain -> requestDomainDnsRefresh(domain.getDomainName()));
}
Optional<Host> optionalHost = tm().transact(() -> tm().loadByKeyIfPresent(hostKey));
if (optionalHost.isEmpty()) {
setFailedStatus(String.format("Host to refresh does not exist: %s", hostKey));
return;
}
Instant now = clock.now();
Host host = optionalHost.get();
if (isDeleted(host, now)) {
setFailedStatus(String.format("Host to refresh is already deleted: %s", host.getHostName()));
return;
}
ImmutableSet<VKey<Domain>> linkedDomainKeys =
getLinkedDomainKeys(hostKey, host.getUpdateTimestamp().getTimestamp(), null);
for (List<VKey<Domain>> batch : Iterables.partition(linkedDomainKeys, DNS_REFRESH_BATCH_SIZE)) {
tm().transact(
() -> {
ImmutableSet<String> domainNames =
tm().loadByKeysIfPresent(batch).values().stream()
.filter(Domain::shouldPublishToDns)
.map(Domain::getDomainName)
.collect(toImmutableSet());
requestDomainDnsRefresh(domainNames);
});
}
response.setStatus(SC_OK);
}
if (!hostValid) {
// Set the response status code to be 204 so to not retry.
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
response.setStatus(SC_NO_CONTENT);
response.setPayload(failureMessage);
}
});
private void setFailedStatus(String message) {
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
response.setStatus(SC_NO_CONTENT);
response.setPayload(message);
}
}
@@ -271,7 +271,7 @@ public interface TransactionManager {
* A runnable that allows for checked exceptions to be thrown.
*
* <p>This makes it easier to write lambdas without having to worry about wrapping and re-throwing
* checked excpetions as unchecked ones.
* checked exceptions as unchecked ones.
*/
@FunctionalInterface
interface ThrowingRunnable {
@@ -15,6 +15,10 @@
package google.registry.batch;
import static com.google.common.truth.Truth.assertThat;
import static com.google.monitoring.metrics.contrib.LongMetricSubject.assertThat;
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.FAILURE;
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.NOT_CONFIGURED;
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.SUCCESS;
import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_DOMAIN_SYNC;
import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_HOST_SYNC;
import static google.registry.testing.DatabaseHelper.createTld;
@@ -73,15 +77,24 @@ class SyncRemoteCacheActionTest {
@BeforeEach
void beforeEach() {
createTld("tld");
SyncRemoteCacheAction.SYNC_CACHE_RUNS_METRIC.reset();
action = new SyncRemoteCacheAction(lockHandler, response, Optional.of(jedisClient));
}
private static void verifyMetrics(SyncRemoteCacheAction.SyncStatus status) {
assertThat(SyncRemoteCacheAction.SYNC_CACHE_RUNS_METRIC)
.hasValueForLabels(1, status.name())
.and()
.hasNoOtherValues();
}
@Test
void test_noJedisConfig() {
action = new SyncRemoteCacheAction(lockHandler, response, Optional.empty());
action.run();
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload()).contains("No Jedis/Valkey configuration found");
verifyMetrics(NOT_CONFIGURED);
}
@Test
@@ -91,6 +104,7 @@ class SyncRemoteCacheActionTest {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload()).contains("Could not acquire lock");
verifyMetrics(FAILURE);
}
@Test
@@ -100,6 +114,7 @@ class SyncRemoteCacheActionTest {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
assertThat(response.getPayload()).contains("Errored out with cause");
verifyMetrics(FAILURE);
}
@Test
@@ -109,6 +124,7 @@ class SyncRemoteCacheActionTest {
verifyNoInteractions(jedisClient);
assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_DOMAIN_SYNC)))
.isEmpty();
verifyMetrics(SUCCESS);
}
@Test
@@ -131,6 +147,7 @@ class SyncRemoteCacheActionTest {
.getCursorTime()
.toString())
.isEqualTo("2025-01-01T00:00:00.001Z");
verifyMetrics(SUCCESS);
}
@Test
@@ -146,6 +163,7 @@ class SyncRemoteCacheActionTest {
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>("active.tld", activeDomain)));
verify(jedisClient).deleteAll(Domain.class, ImmutableList.of("deleted.tld"));
verifyMetrics(SUCCESS);
}
@Test
@@ -166,6 +184,7 @@ class SyncRemoteCacheActionTest {
verify(jedisClient)
.setAll(
ImmutableList.of(new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2)));
verifyMetrics(SUCCESS);
}
@Test
@@ -175,6 +194,7 @@ class SyncRemoteCacheActionTest {
verifyNoInteractions(jedisClient);
assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_HOST_SYNC)))
.isEmpty();
verifyMetrics(SUCCESS);
}
@Test
@@ -197,6 +217,7 @@ class SyncRemoteCacheActionTest {
.getCursorTime()
.toString())
.isEqualTo("2025-01-01T00:00:00.001Z");
verifyMetrics(SUCCESS);
}
@Test
@@ -212,5 +233,6 @@ class SyncRemoteCacheActionTest {
ImmutableList.of(
new SimplifiedJedisClient.JedisResource<>(active.getRepoId(), active)));
verify(jedisClient).deleteAll(Host.class, ImmutableList.of(deleted.getRepoId()));
verifyMetrics(SUCCESS);
}
}
@@ -28,6 +28,7 @@ import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.Host;
import google.registry.persistence.transaction.JpaTestExtensions;
@@ -52,7 +53,7 @@ public class RefreshDnsOnHostRenameActionTest {
private RefreshDnsOnHostRenameAction action;
private void createAction(String hostKey) {
action = new RefreshDnsOnHostRenameAction(hostKey, response);
action = new RefreshDnsOnHostRenameAction(hostKey, response, clock);
}
@BeforeEach
@@ -99,4 +100,28 @@ public class RefreshDnsOnHostRenameActionTest {
assertThat(response.getPayload())
.isEqualTo("Host to refresh is already deleted: ns1.example.tld");
}
@Test
void testSuccess_multipleBatches() {
Host host = persistActiveHost("ns1.example.tld");
ImmutableSet.Builder<String> domainNamesBuilder = new ImmutableSet.Builder<>();
for (int i = 1; i <= 1001; i++) {
String domainName = "example" + i + ".tld";
domainNamesBuilder.add(domainName);
persistResource(newDomain(domainName, host));
}
createAction(host.createVKey().stringify());
action.run();
assertDomainDnsRequests(Iterables.toArray(domainNamesBuilder.build(), String.class));
assertThat(response.getStatus()).isEqualTo(SC_OK);
}
@Test
void testSuccess_noLinkedDomains() {
Host host = persistActiveHost("ns1.example.tld");
createAction(host.createVKey().stringify());
action.run();
assertNoDnsRequests();
assertThat(response.getStatus()).isEqualTo(SC_OK);
}
}
+1
View File
@@ -84,6 +84,7 @@ steps:
sed -i "s|artifactStorage: artifactStorage|artifactStorage: $artifact_storage|" "$target_file"
sed -i "s|serviceAccount: serviceAccount|serviceAccount: $service_account|" "$target_file"
sed -i "s|serviceAccount: serviceAccount|serviceAccount: $service_account|" release/clouddeploy/delivery-pipeline.yaml
sed -i "s|cluster: cluster|cluster: $cluster_val|" "$target_file"
sed -i "s|workerPool: workerPool|workerPool: $worker_pool|" "$target_file"
fi
+1 -1
View File
@@ -203,7 +203,7 @@ steps:
--images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest}" \
--source=. \
--skaffold-file=release/clouddeploy/skaffold.yaml \
--deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus"
--deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus,tag_name=${TAG_NAME},project_id=${PROJECT_ID}"
# The tarballs and jars to upload to GCS.
artifacts:
objects:
+12 -21
View File
@@ -236,11 +236,17 @@ steps:
# partial phase manifests
for stage in 1 5
do
if [ ${service} == backend ] || [ ${service} == console ]
then
replicas=1
else
replicas=${stage}
fi
awk 'NR==1,/^---$/ {if ($0 != "---") print}' ./jetty/kubernetes/nomulus-${env}-${service}.yaml | \
sed s/name:\ ${service}/name:\ ${service}-partial-phase/g | \
sed s/service:\ ${service}/deployment:\ ${service}-partial-phase/g | \
sed s/value:\ ${service}/value:\ ${service}-partial-phase/g | \
sed "/^spec:$/a\ replicas: ${stage}" \
sed "/^spec:$/a\ replicas: ${replicas}" \
> ./jetty/kubernetes/nomulus-${env}-${service}-partial-phase-${stage}.yaml
done
# gateway
@@ -254,26 +260,11 @@ steps:
> ./jetty/kubernetes/gateway/nomulus-backend-policy-${env}-${service}-canary.yaml
done
# Generate manifests for epp-server (which doesn't use HTTP gateway routing)
for env in alpha crash qa sandbox production
do
if [ ${env} == production ]
then
project="domain-registry"
else
project="domain-registry-${env}"
fi
# non-canary
sed s/GCP_PROJECT/${PROJECT_ID}/g ./jetty/kubernetes/nomulus-epp-server.yaml | \
sed s/latest/${TAG_NAME}/g | \
sed s/ENVIRONMENT/${env}/g > ./jetty/kubernetes/nomulus-${env}-epp-server.yaml
# canary
sed s/GCP_PROJECT/${PROJECT_ID}/g ./jetty/kubernetes/nomulus-epp-server.yaml | \
sed s/latest/${TAG_NAME}/g | \
sed s/ENVIRONMENT/${env}/g | \
sed s/epp-server/epp-server-canary/g | \
sed s/EPP-v2-ipv4-main/EPP-v2-ipv4-canary/g | \
sed s/EPP-v2-ipv6-main/EPP-v2-ipv6-canary/g > ./jetty/kubernetes/nomulus-${env}-epp-server-canary.yaml
done
# Lowercase EPP-v2 to epp-v2 to match reserved static GCE IP names.
sed s/GCP_PROJECT/${PROJECT_ID}/g ./jetty/kubernetes/nomulus-epp-server.yaml | \
sed s/latest/${TAG_NAME}/g | \
sed s/ENVIRONMENT/${env}/g | \
sed s/EPP-v2/epp-v2/g > ./jetty/kubernetes/nomulus-${env}-epp-server.yaml
done
# Upload the Gradle binary to GCS if it does not exist and point URL in Gradle wrapper to it.
- name: 'gcr.io/cloud-builders/gcloud'
+1 -1
View File
@@ -5,7 +5,7 @@ This directory contains the Google Cloud Deploy configuration files for the Nomu
## Files
### `delivery-pipeline.yaml`
Defines the `DeliveryPipeline` resource named `deploy-nomulus`. It sets up the serial pipeline for rolling out changes to different targets.
Defines the `DeliveryPipeline` resource named `deploy-nomulus` and its associated `Automation` resource (`deploy-nomulus/auto-advance-canary`). It sets up the serial pipeline for rolling out changes to different targets and automatically advancing canary rollouts.
### Target Configurations (e.g., `crash-target.yaml`)
Files matching this format define the `Target` resources for Cloud Deploy. They specify the GKE cluster and other environment-specific settings for deployment.
+35 -18
View File
@@ -14,17 +14,6 @@ serialPipeline:
- phaseId: "canary-1"
profiles: ["crash-partial-phase-1"]
percentage: 10
predeploy:
tasks:
- type: container
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-verify-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
@@ -55,17 +44,20 @@ serialPipeline:
args:
- "-c"
- |
gcloud artifacts docker tags add $DEPLOYED_IMAGE \
${BASE_IMAGE}:live-cd-${TARGET_ID}
gcloud container images add-tag $DEPLOYED_IMAGE \
${BASE_IMAGE}:live-cd-${TARGET_ID} --quiet
- type: container
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
TAG_NAME: ${{ deploy_params['tag_name'] }}
PROJECT_ID: ${{ deploy_params['project_id'] }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-deploy-${TARGET_ID}.yaml
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-deploy-${TARGET_ID}.yaml .
gcloud builds submit --no-source --config=cloudbuild-schema-deploy-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
@@ -86,11 +78,14 @@ serialPipeline:
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
TAG_NAME: ${{ deploy_params['tag_name'] }}
PROJECT_ID: ${{ deploy_params['project_id'] }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-verify-${TARGET_ID}.yaml
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-verify-${TARGET_ID}.yaml .
gcloud builds submit --no-source --config=cloudbuild-schema-verify-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
@@ -121,20 +116,42 @@ serialPipeline:
args:
- "-c"
- |
gcloud artifacts docker tags add $DEPLOYED_IMAGE \
${BASE_IMAGE}:live-cd-${TARGET_ID}
gcloud container images add-tag $DEPLOYED_IMAGE \
${BASE_IMAGE}:live-cd-${TARGET_ID} --quiet
- type: container
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
TAG_NAME: ${{ deploy_params['tag_name'] }}
PROJECT_ID: ${{ deploy_params['project_id'] }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-deploy-${TARGET_ID}.yaml
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-deploy-${TARGET_ID}.yaml .
gcloud builds submit --no-source --config=cloudbuild-schema-deploy-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
googleCloud:
alertPolicyChecks:
sandboxStableDeploymentAlertPolicyChecks
---
apiVersion: deploy.cloud.google.com/v1
kind: Automation
metadata:
name: deploy-nomulus/auto-advance-canary
description: Automatically advances rollouts through canary-1 phase after successful deployment and analysis.
# Placeholder: Replace with project service account.
serviceAccount: serviceAccount
selector:
targets:
- id: crash
- id: sandbox
rules:
- advanceRolloutRule:
id: advance-canary-phases
sourcePhases:
- "canary-1"
wait: 0m