summaryrefslogtreecommitdiffstats
path: root/configserver
diff options
context:
space:
mode:
authorHarald Musum <musum@yahooinc.com>2023-08-28 12:46:45 +0200
committerHarald Musum <musum@yahooinc.com>2023-08-28 12:46:45 +0200
commit12bba1e9531d8237e103a8850969baf99da950df (patch)
treef650614988367e99255f01f4463f24c646910ed6 /configserver
parentfce91af952e5a5e2fe21c319bc259e0dd4ae5640 (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')
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationCuratorDatabase.java83
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationData.java72
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantApplications.java36
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java21
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationCuratorDatabaseTest.java74
5 files changed, 272 insertions, 14 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;
}
/**
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationCuratorDatabaseTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationCuratorDatabaseTest.java
index cbdb462c35e..8d1b22c94c5 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationCuratorDatabaseTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationCuratorDatabaseTest.java
@@ -3,13 +3,17 @@ package com.yahoo.vespa.config.server.application;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.curator.transaction.CuratorTransaction;
import org.junit.Test;
import java.time.Instant;
import java.util.Optional;
+import java.util.OptionalLong;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
/**
* @author jonmv
@@ -35,4 +39,72 @@ public class ApplicationCuratorDatabaseTest {
assertEquals(reindexing, db.readReindexingStatus(id).orElseThrow());
}
+ @Test
+ public void testReadingAndWritingApplicationData() {
+ ApplicationId id = ApplicationId.defaultId();
+ MockCurator curator = new MockCurator();
+ ApplicationCuratorDatabase db = new ApplicationCuratorDatabase(id.tenant(), curator);
+
+ assertEquals(Optional.empty(), db.applicationData(id));
+
+ db.createApplication(id, false);
+ assertEquals(Optional.empty(), db.applicationData(id)); // still empty, as no data has been written to node
+
+ db.createApplication(id, true);
+ try {
+ Optional<ApplicationData> applicationData = db.applicationData(id);
+ fail("Expected exception, got " + applicationData);
+ } catch (NumberFormatException e) {
+ // expected
+ }
+
+ // Can be read as json, but no active session or last deployed session
+ Optional<ApplicationData> applicationData = db.applicationData(id, true);
+ assertTrue(applicationData.isPresent());
+ assertEquals(id, applicationData.get().applicationId());
+ assertFalse(applicationData.get().activeSession().isPresent());
+ assertFalse(applicationData.get().lastDeployedSession().isPresent());
+
+ // Prepare session 2, no active session
+ try (var t = db.createWritePrepareTransaction(new CuratorTransaction(curator), id, 2, OptionalLong.empty(), false)) {
+ t.commit();
+ }
+ // Activate session 2, last deployed session not present (not writing json)
+ try (var t = db.createWriteActiveTransaction(new CuratorTransaction(curator), id, 2, false)) {
+ t.commit();
+ }
+ // Can be read as session id only
+ applicationData = db.applicationData(id, false);
+ assertTrue(applicationData.isPresent());
+ assertEquals(id, applicationData.get().applicationId());
+ assertTrue(applicationData.get().activeSession().isPresent());
+ assertEquals(2, applicationData.get().activeSession().getAsLong());
+ assertFalse(applicationData.get().lastDeployedSession().isPresent());
+
+ // Prepare session 3, last deployed session is still 2
+ try (var t = db.createWritePrepareTransaction(new CuratorTransaction(curator), id, 3, OptionalLong.of(2), true)) {
+ t.commit();
+ }
+ // Can be read as json, active session is still 2 and last deployed session is 3
+ applicationData = db.applicationData(id, true);
+ assertTrue(applicationData.isPresent());
+ assertEquals(id, applicationData.get().applicationId());
+ assertTrue(applicationData.get().activeSession().isPresent());
+ assertEquals(2, applicationData.get().activeSession().getAsLong());
+ assertTrue(applicationData.get().lastDeployedSession().isPresent());
+ assertEquals(3, applicationData.get().lastDeployedSession().getAsLong());
+
+ try (var t = db.createWriteActiveTransaction(new CuratorTransaction(curator), id, 3, true)) {
+ t.commit();
+ }
+ // Can be read as json, active session and last deployed session present
+ applicationData = db.applicationData(id, true);
+ assertTrue(applicationData.isPresent());
+ assertEquals(id, applicationData.get().applicationId());
+ assertTrue(applicationData.get().activeSession().isPresent());
+ assertEquals(3, applicationData.get().activeSession().getAsLong());
+ assertTrue(applicationData.get().lastDeployedSession().isPresent());
+ assertEquals(3, applicationData.get().lastDeployedSession().getAsLong());
+ }
+
}