diff options
author | Harald Musum <musum@yahooinc.com> | 2023-08-28 12:46:45 +0200 |
---|---|---|
committer | Harald Musum <musum@yahooinc.com> | 2023-08-28 12:46:45 +0200 |
commit | 12bba1e9531d8237e103a8850969baf99da950df (patch) | |
tree | f650614988367e99255f01f4463f24c646910ed6 /configserver/src/main/java/com | |
parent | fce91af952e5a5e2fe21c319bc259e0dd4ae5640 (diff) |
Support reading and writing application data as json
Controlled by feature flags.
Also write last deployed session when preparing a session
Diffstat (limited to 'configserver/src/main/java/com')
4 files changed, 199 insertions, 13 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationCuratorDatabase.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationCuratorDatabase.java index bd2bad4db93..70b8dc5f51a 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationCuratorDatabase.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationCuratorDatabase.java @@ -23,9 +23,11 @@ import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Optional; +import java.util.OptionalLong; import java.util.concurrent.ExecutorService; import java.util.function.UnaryOperator; +import static com.yahoo.vespa.curator.transaction.CuratorOperations.setData; import static java.util.stream.Collectors.toUnmodifiableMap; /** @@ -72,13 +74,18 @@ public class ApplicationCuratorDatabase { /** * Creates a node for the given application, marking its existence. */ - public void createApplication(ApplicationId id) { + public void createApplication(ApplicationId id, boolean writeAsJson) { if ( ! id.tenant().equals(tenant)) throw new IllegalArgumentException("Cannot write application id '" + id + "' for tenant '" + tenant + "'"); - try (Lock lock = lock(id)) { - if (curator.exists(applicationPath(id))) return; - curator.create(applicationPath(id)); + try (Lock lock = lock(id)) { + if (writeAsJson) { + var applicationData = new ApplicationData(id, OptionalLong.empty(), OptionalLong.empty()); + curator.set(applicationPath(id), applicationData.toJson()); + } else { + if (curator.exists(applicationPath(id))) return; + curator.create(applicationPath(id)); + } modifyReindexing(id, ApplicationReindexing.empty(), UnaryOperator.identity()); } } @@ -89,8 +96,32 @@ public class ApplicationCuratorDatabase { * @param applicationId An {@link ApplicationId} that represents an active application. * @param sessionId session id belonging to the application package for this application id. */ - public Transaction createWriteActiveTransaction(Transaction transaction, ApplicationId applicationId, long sessionId) { - return transaction.add(CuratorOperations.setData(applicationPath(applicationId).getAbsolute(), Utf8.toAsciiBytes(sessionId))); + public Transaction createWriteActiveTransaction(Transaction transaction, ApplicationId applicationId, long sessionId, boolean writeAsJson) { + String path = applicationPath(applicationId).getAbsolute(); + return transaction.add(writeAsJson + ? setData(path, new ApplicationData(applicationId, OptionalLong.of(sessionId), OptionalLong.of(sessionId)).toJson()) + : setData(path, Utf8.toAsciiBytes(sessionId))); + } + + /** + * Returns a transaction which writes the given session id as the currently active for the given application. + * + * @param applicationId An {@link ApplicationId} that represents an active application. + * @param sessionId session id belonging to the application package for this application id. + */ + public Transaction createWritePrepareTransaction(Transaction transaction, + ApplicationId applicationId, + long sessionId, + OptionalLong activeSessionId, + boolean writeAsJson) { + + // Needs to read or be supplied current active session id, to avoid overwriting a newer session id. + + String path = applicationPath(applicationId).getAbsolute(); + if (writeAsJson) + return transaction.add(setData(path, new ApplicationData(applicationId, activeSessionId, OptionalLong.of(sessionId)).toJson())); + else + return transaction; // Do nothing, as there is nothing to write in this case } /** @@ -112,6 +143,46 @@ public class ApplicationCuratorDatabase { } /** + * Returns application data for the given application. + * Returns Optional.empty() if application not found or no application data exists. + */ + public Optional<ApplicationData> applicationData(ApplicationId id) { + return applicationData(id, false); + } + + /** + * Returns application data for the given application. + * Returns Optional.empty() if application not found or no application data exists. + */ + public Optional<ApplicationData> applicationData(ApplicationId id, boolean readAsJson) { + Optional<byte[]> data = curator.getData(applicationPath(id)); + if (data.isEmpty() || data.get().length == 0) return Optional.empty(); + + if (readAsJson) { + try { + return Optional.of(ApplicationData.fromBytes(data.get())); + } catch (IllegalArgumentException e) { + return applicationDataOldFormat(id, readAsJson); + } + } else { + return applicationDataOldFormat(id, readAsJson); + } + } + + /** + * Returns application data for the given application. + * Returns Optional.empty() if application not found or no application data exists. + */ + public Optional<ApplicationData> applicationDataOldFormat(ApplicationId id, boolean readAsJson) { + Optional<byte[]> data = curator.getData(applicationPath(id)); + if (data.isEmpty() || data.get().length == 0) return Optional.empty(); + + return Optional.of(new ApplicationData(id, + OptionalLong.of(data.map(bytes -> Long.parseLong(Utf8.toString(bytes))).get()), + OptionalLong.empty())); + } + + /** * List the active applications of a tenant in this config server. * * @return a list of {@link ApplicationId}s that are active. diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationData.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationData.java new file mode 100644 index 00000000000..868ea060fba --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationData.java @@ -0,0 +1,72 @@ +package com.yahoo.vespa.config.server.application; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; + +import java.io.IOException; +import java.util.OptionalLong; + +import static com.yahoo.slime.SlimeUtils.optionalLong; + +/** + * Data class for application id, active session and last deployed session + * + * @author hmusum + */ +public class ApplicationData { + + private static final String APPLICATION_ID_FIELD = "applicationId"; + private static final String ACTIVE_SESSION_FIELD = "activeSession"; + private static final String LAST_DEPLOYED_SESSION_FIELD = "lastDeployedSession"; + + private final ApplicationId applicationId; + private final OptionalLong activeSession; + private final OptionalLong lastDeployedSession; + + ApplicationData(ApplicationId applicationId, OptionalLong activeSession, OptionalLong lastDeployedSession) { + this.applicationId = applicationId; + this.activeSession = activeSession; + this.lastDeployedSession = lastDeployedSession; + } + + static ApplicationData fromBytes(byte[] data) { + return fromSlime(SlimeUtils.jsonToSlime(data)); + } + + static ApplicationData fromSlime(Slime slime) { + Cursor cursor = slime.get(); + return new ApplicationData(ApplicationId.fromSerializedForm(cursor.field(APPLICATION_ID_FIELD).asString()), + optionalLong(cursor.field(ACTIVE_SESSION_FIELD)), + optionalLong(cursor.field(LAST_DEPLOYED_SESSION_FIELD))); + } + + public byte[] toJson() { + try { + Slime slime = new Slime(); + toSlime(slime.setObject()); + return SlimeUtils.toJsonBytes(slime); + } catch (IOException e) { + throw new RuntimeException("Serialization of application data to json failed", e); + } + } + + public ApplicationId applicationId() { return applicationId; } + + public OptionalLong activeSession() { return activeSession; } + + public OptionalLong lastDeployedSession() { return lastDeployedSession; } + + @Override + public String toString() { + return "application '" + applicationId + "', active session " + activeSession + ", last deployed session " + lastDeployedSession; + } + + private void toSlime(Cursor object) { + object.setString(APPLICATION_ID_FIELD, applicationId.serializedForm()); + activeSession.ifPresent(session -> object.setLong(ACTIVE_SESSION_FIELD, session)); + lastDeployedSession.ifPresent(session -> object.setLong(LAST_DEPLOYED_SESSION_FIELD, session)); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantApplications.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantApplications.java index af29f6e8530..538534d0040 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantApplications.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantApplications.java @@ -27,7 +27,9 @@ import com.yahoo.vespa.curator.CompletionTimeoutException; import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.curator.transaction.CuratorTransaction; +import com.yahoo.vespa.flags.BooleanFlag; import com.yahoo.vespa.flags.FlagSource; +import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.flags.ListFlag; import com.yahoo.vespa.flags.PermanentFlags; import org.apache.curator.framework.CuratorFramework; @@ -41,6 +43,7 @@ import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; +import java.util.OptionalLong; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; @@ -76,6 +79,8 @@ public class TenantApplications implements RequestHandler, HostValidator { private final TenantFileSystemDirs tenantFileSystemDirs; private final String serverId; private final ListFlag<String> incompatibleVersions; + private final BooleanFlag writeApplicationDataAsJson; + private final BooleanFlag readApplicationDataAsJson; public TenantApplications(TenantName tenant, Curator curator, StripedExecutor<TenantName> zkWatcherExecutor, ExecutorService zkCacheExecutor, Metrics metrics, ConfigActivationListener configActivationListener, @@ -97,6 +102,8 @@ public class TenantApplications implements RequestHandler, HostValidator { this.clock = clock; this.serverId = configserverConfig.serverId(); this.incompatibleVersions = PermanentFlags.INCOMPATIBLE_VERSIONS.bindTo(flagSource); + this.writeApplicationDataAsJson = Flags.WRITE_APPLICATION_DATA_AS_JSON.bindTo(flagSource); + this.readApplicationDataAsJson = Flags.READ_APPLICATION_DATA_AS_JSON.bindTo(flagSource); } /** The curator backed ZK storage of this. */ @@ -123,6 +130,14 @@ public class TenantApplications implements RequestHandler, HostValidator { return database().activeSessionOf(id); } + /** + * Returns application data for the given application. + * Returns Optional.empty if application not found or no application data exists. + */ + public Optional<ApplicationData> applicationData(ApplicationId id) { + return database().applicationData(id, readApplicationDataAsJson.value()); + } + public boolean sessionExistsInFileSystem(long sessionId) { return Files.exists(Paths.get(tenantFileSystemDirs.sessionsPath().getAbsolutePath(), String.valueOf(sessionId))); } @@ -134,14 +149,31 @@ public class TenantApplications implements RequestHandler, HostValidator { * @param sessionId session id belonging to the application package for this application id. */ public Transaction createWriteActiveTransaction(Transaction transaction, ApplicationId applicationId, long sessionId) { - return database().createWriteActiveTransaction(transaction, applicationId, sessionId); + return database().createWriteActiveTransaction(transaction, applicationId, sessionId, writeApplicationDataAsJson.value()); + } + + /** + * Returns a transaction which writes the given session id as the last deployed for the given application. + * + * @param applicationId An {@link ApplicationId} that represents an active application. + * @param sessionId session id belonging to the application package for this application id. + */ + public Transaction createWritePrepareTransaction(Transaction transaction, + ApplicationId applicationId, + long sessionId, + Optional<Long> activeSessionId) { + return database().createWritePrepareTransaction(transaction, + applicationId, + sessionId, + activeSessionId.map(OptionalLong::of).orElseGet(OptionalLong::empty), + writeApplicationDataAsJson.value()); } /** * Creates a node for the given application, marking its existence. */ public void createApplication(ApplicationId id) { - database().createApplication(id); + database().createApplication(id, writeApplicationDataAsJson.value()); } /** diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java index 3144665ec91..44a656a1579 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java @@ -39,6 +39,7 @@ import com.yahoo.vespa.config.server.tenant.TenantRepository; import com.yahoo.vespa.config.server.zookeeper.SessionCounter; import com.yahoo.vespa.config.server.zookeeper.ZKApplication; import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.transaction.CuratorTransaction; import com.yahoo.vespa.flags.BooleanFlag; import com.yahoo.vespa.flags.FlagSource; import com.yahoo.vespa.flags.Flags; @@ -239,14 +240,22 @@ public class SessionRepository { throw new UnknownVespaVersionException("Vespa version '" + version + "' not known by this config server"); }); - applicationRepo.createApplication(params.getApplicationId()); // TODO jvenstad: This is wrong, but it has to be done now, since preparation can change the application ID of a session :( - logger.log(Level.FINE, "Created application " + params.getApplicationId()); + ApplicationId applicationId = params.getApplicationId(); + applicationRepo.createApplication(applicationId); // TODO jvenstad: This is wrong, but it has to be done now, since preparation can change the application ID of a session :( + logger.log(Level.FINE, "Created application " + applicationId); long sessionId = session.getSessionId(); SessionZooKeeperClient sessionZooKeeperClient = createSessionZooKeeperClient(sessionId); Optional<CompletionWaiter> waiter = params.isDryRun() ? Optional.empty() : Optional.of(sessionZooKeeperClient.createPrepareWaiter()); - Optional<ApplicationVersions> activeApplicationVersions = activeApplicationVersions(params.getApplicationId()); + Optional<ApplicationVersions> activeApplicationVersions = activeApplicationVersions(applicationId); + try (var transaction = new CuratorTransaction(curator)) { + applicationRepo.createWritePrepareTransaction(transaction, + applicationId, + sessionId, + getActiveSessionId(applicationId)) + .commit(); + } ConfigChangeActions actions = sessionPreparer.prepare(applicationRepo, logger, params, activeApplicationVersions, now, getSessionAppDir(sessionId), session.getApplicationPackage(), sessionZooKeeperClient) @@ -277,6 +286,7 @@ public class SessionRepository { timeoutBudget, deployLogger, created); + applicationRepo.createApplication(applicationId); write(existingSession, session, applicationId, created); return session; } @@ -293,9 +303,10 @@ public class SessionRepository { ApplicationId applicationId, TimeoutBudget timeoutBudget, DeployLogger deployLogger) { - applicationRepo.createApplication(applicationId); - return createSessionFromApplication(applicationDirectory, applicationId, false, timeoutBudget, + LocalSession session = createSessionFromApplication(applicationDirectory, applicationId, false, timeoutBudget, deployLogger, clock.instant()); + applicationRepo.createApplication(applicationId); + return session; } /** |