diff options
19 files changed, 721 insertions, 620 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java index af5a561aa6e..dd88096c62f 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java @@ -49,9 +49,10 @@ import com.yahoo.vespa.config.server.http.v2.PrepareResult; import com.yahoo.vespa.config.server.metrics.ApplicationMetricsRetriever; import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; import com.yahoo.vespa.config.server.session.LocalSession; -import com.yahoo.vespa.config.server.session.SessionRepository; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; import com.yahoo.vespa.config.server.session.PrepareParams; import com.yahoo.vespa.config.server.session.RemoteSession; +import com.yahoo.vespa.config.server.session.RemoteSessionRepo; import com.yahoo.vespa.config.server.session.Session; import com.yahoo.vespa.config.server.session.SessionFactory; import com.yahoo.vespa.config.server.session.SilentDeployLogger; @@ -304,7 +305,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye if (activeSession == null) return Optional.empty(); TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout); LocalSession newSession = tenant.getSessionFactory().createSessionFromExisting(activeSession, logger, true, timeoutBudget); - tenant.getSessionRepository().addSession(newSession); + tenant.getLocalSessionRepo().addSession(newSession); return Optional.of(Deployment.unprepared(newSession, this, hostProvisioner, tenant, timeout, clock, false /* don't validate as this is already deployed */, bootstrap)); @@ -488,7 +489,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye Tenant tenant = tenantRepository.getTenant(applicationId.tenant()); if (tenant == null) throw new NotFoundException("Tenant '" + applicationId.tenant() + "' not found"); long sessionId = getSessionIdForApplication(tenant, applicationId); - RemoteSession session = tenant.getSessionRepo().getRemoteSession(sessionId); + RemoteSession session = tenant.getRemoteSessionRepo().getSession(sessionId); if (session == null) throw new NotFoundException("Remote session " + sessionId + " not found"); return session.ensureApplicationLoaded().getForVersionOrLatest(version, clock.instant()); } catch (NotFoundException e) { @@ -525,10 +526,10 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye } private boolean localSessionHasBeenDeleted(ApplicationId applicationId, long sessionId, Duration waitTime) { - SessionRepository sessionRepository = tenantRepository.getTenant(applicationId.tenant()).getSessionRepo(); + RemoteSessionRepo remoteSessionRepo = tenantRepository.getTenant(applicationId.tenant()).getRemoteSessionRepo(); Instant end = Instant.now().plus(waitTime); do { - if (sessionRepository.getRemoteSession(sessionId) == null) return true; + if (remoteSessionRepo.getSession(sessionId) == null) return true; try { Thread.sleep(10); } catch (InterruptedException e) { /* ignored */} } while (Instant.now().isBefore(end)); @@ -634,11 +635,11 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye boolean internalRedeploy, TimeoutBudget timeoutBudget) { Tenant tenant = tenantRepository.getTenant(applicationId.tenant()); - SessionRepository sessionRepository = tenant.getSessionRepository(); + LocalSessionRepo localSessionRepo = tenant.getLocalSessionRepo(); SessionFactory sessionFactory = tenant.getSessionFactory(); RemoteSession fromSession = getExistingSession(tenant, applicationId); LocalSession session = sessionFactory.createSessionFromExisting(fromSession, logger, internalRedeploy, timeoutBudget); - sessionRepository.addSession(session); + localSessionRepo.addSession(session); return session.getSessionId(); } @@ -659,13 +660,13 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye Optional<Long> activeSessionId = tenant.getApplicationRepo().activeSessionOf(applicationId); LocalSession session = tenant.getSessionFactory().createSession(applicationDirectory, applicationId, timeoutBudget, activeSessionId); - tenant.getSessionRepository().addSession(session); + tenant.getLocalSessionRepo().addSession(session); return session.getSessionId(); } public void deleteExpiredLocalSessions() { Map<Tenant, List<LocalSession>> sessionsPerTenant = new HashMap<>(); - tenantRepository.getAllTenants().forEach(tenant -> sessionsPerTenant.put(tenant, tenant.getSessionRepository().getSessions())); + tenantRepository.getAllTenants().forEach(tenant -> sessionsPerTenant.put(tenant, tenant.getLocalSessionRepo().getSessions())); Set<ApplicationId> applicationIds = new HashSet<>(); sessionsPerTenant.values().forEach(sessionList -> sessionList.forEach(s -> applicationIds.add(s.getApplicationId()))); @@ -676,7 +677,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye if (activeSession != null) activeSessions.put(applicationId, activeSession.getSessionId()); }); - sessionsPerTenant.keySet().forEach(tenant -> tenant.getSessionRepository().deleteExpiredSessions(activeSessions)); + sessionsPerTenant.keySet().forEach(tenant -> tenant.getLocalSessionRepo().deleteExpiredSessions(activeSessions)); } public int deleteExpiredRemoteSessions(Duration expiryTime) { @@ -686,7 +687,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye public int deleteExpiredRemoteSessions(Clock clock, Duration expiryTime) { return tenantRepository.getAllTenants() .stream() - .map(tenant -> tenant.getSessionRepo().deleteExpiredRemoteSessions(clock, expiryTime)) + .map(tenant -> tenant.getRemoteSessionRepo().deleteExpiredSessions(clock, expiryTime)) .mapToInt(i -> i) .sum(); } @@ -746,14 +747,14 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye } private LocalSession getLocalSession(Tenant tenant, long sessionId) { - LocalSession session = tenant.getSessionRepository().getSession(sessionId); + LocalSession session = tenant.getLocalSessionRepo().getSession(sessionId); if (session == null) throw new NotFoundException("Session " + sessionId + " was not found"); return session; } private RemoteSession getRemoteSession(Tenant tenant, long sessionId) { - RemoteSession session = tenant.getSessionRepo().getRemoteSession(sessionId); + RemoteSession session = tenant.getRemoteSessionRepo().getSession(sessionId); if (session == null) throw new NotFoundException("Session " + sessionId + " was not found"); return session; @@ -804,7 +805,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye private RemoteSession getActiveSession(Tenant tenant, ApplicationId applicationId) { TenantApplications applicationRepo = tenant.getApplicationRepo(); if (applicationRepo.activeApplications().contains(applicationId)) { - return tenant.getSessionRepo().getRemoteSession(applicationRepo.requireActiveSessionOf(applicationId)); + return tenant.getRemoteSessionRepo().getSession(applicationRepo.requireActiveSessionOf(applicationId)); } return null; } @@ -812,7 +813,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye public LocalSession getActiveLocalSession(Tenant tenant, ApplicationId applicationId) { TenantApplications applicationRepo = tenant.getApplicationRepo(); if (applicationRepo.activeApplications().contains(applicationId)) { - return tenant.getSessionRepository().getSession(applicationRepo.requireActiveSessionOf(applicationId)); + return tenant.getLocalSessionRepo().getSession(applicationRepo.requireActiveSessionOf(applicationId)); } return null; } 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 d4fe35c14b1..a4dfec708d6 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 @@ -213,7 +213,7 @@ public class TenantApplications implements RequestHandler, ReloadHandler, HostVa break; } // We may have lost events and may need to remove applications. - // New applications are added when session is added, not here. See SessionRepository. + // New applications are added when session is added, not here. See RemoteSessionRepo. removeUnusedApplications(); }); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java new file mode 100644 index 00000000000..e23552dee44 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java @@ -0,0 +1,151 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.path.Path; +import com.yahoo.transaction.NestedTransaction; +import com.yahoo.vespa.config.server.GlobalComponentRegistry; +import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; +import com.yahoo.vespa.config.server.tenant.TenantRepository; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import com.yahoo.vespa.curator.Curator; + +import java.io.File; +import java.io.FilenameFilter; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * Contains state for the local instance of the configserver. + * + * @author Ulf Lilleengen + */ +public class LocalSessionRepo { + + private static final Logger log = Logger.getLogger(LocalSessionRepo.class.getName()); + private static final FilenameFilter sessionApplicationsFilter = (dir, name) -> name.matches("\\d+"); + + private final SessionCache<LocalSession> sessionCache; + private final Map<Long, LocalSessionStateWatcher> sessionStateWatchers = new HashMap<>(); + private final Duration sessionLifetime; + private final Clock clock; + private final Curator curator; + private final Executor zkWatcherExecutor; + private final TenantFileSystemDirs tenantFileSystemDirs; + + public LocalSessionRepo(TenantName tenantName, GlobalComponentRegistry componentRegistry, SessionFactory sessionFactory) { + sessionCache = new SessionCache<>(); + this.clock = componentRegistry.getClock(); + this.curator = componentRegistry.getCurator(); + this.sessionLifetime = Duration.ofSeconds(componentRegistry.getConfigserverConfig().sessionLifetime()); + this.zkWatcherExecutor = command -> componentRegistry.getZkWatcherExecutor().execute(tenantName, command); + this.tenantFileSystemDirs = new TenantFileSystemDirs(componentRegistry.getConfigServerDB(), tenantName); + loadSessions(sessionFactory); + } + + public synchronized void addSession(LocalSession session) { + sessionCache.addSession(session); + Path sessionsPath = TenantRepository.getSessionsPath(session.getTenantName()); + long sessionId = session.getSessionId(); + Curator.FileCache fileCache = curator.createFileCache(sessionsPath.append(String.valueOf(sessionId)).append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH).getAbsolute(), false); + sessionStateWatchers.put(sessionId, new LocalSessionStateWatcher(fileCache, session, this, zkWatcherExecutor)); + } + + public LocalSession getSession(long sessionId) { + return sessionCache.getSession(sessionId); + } + + public List<LocalSession> getSessions() { + return sessionCache.getSessions(); + } + + private void loadSessions(SessionFactory sessionFactory) { + File[] sessions = tenantFileSystemDirs.sessionsPath().listFiles(sessionApplicationsFilter); + if (sessions == null) { + return; + } + for (File session : sessions) { + try { + addSession(sessionFactory.createSessionFromId(Long.parseLong(session.getName()))); + } catch (IllegalArgumentException e) { + log.log(Level.WARNING, "Could not load session '" + + session.getAbsolutePath() + "':" + e.getMessage() + ", skipping it."); + } + } + } + + public void deleteExpiredSessions(Map<ApplicationId, Long> activeSessions) { + log.log(Level.FINE, "Purging old sessions"); + try { + for (LocalSession candidate : sessionCache.getSessions()) { + Instant createTime = candidate.getCreateTime(); + log.log(Level.FINE, "Candidate session for deletion: " + candidate.getSessionId() + ", created: " + createTime); + + // Sessions with state other than ACTIVATED + if (hasExpired(candidate) && !isActiveSession(candidate)) { + deleteSession(candidate); + } else if (createTime.plus(Duration.ofDays(1)).isBefore(clock.instant())) { + // Sessions with state ACTIVATE, but which are not actually active + ApplicationId applicationId = candidate.getApplicationId(); + Long activeSession = activeSessions.get(applicationId); + if (activeSession == null || activeSession != candidate.getSessionId()) { + deleteSession(candidate); + log.log(Level.INFO, "Deleted inactive session " + candidate.getSessionId() + " created " + + createTime + " for '" + applicationId + "'"); + } + } + } + // Make sure to catch here, to avoid executor just dying in case of issues ... + } catch (Throwable e) { + log.log(Level.WARNING, "Error when purging old sessions ", e); + } + log.log(Level.FINE, "Done purging old sessions"); + } + + private boolean hasExpired(LocalSession candidate) { + return (candidate.getCreateTime().plus(sessionLifetime).isBefore(clock.instant())); + } + + private boolean isActiveSession(LocalSession candidate) { + return candidate.getStatus() == Session.Status.ACTIVATE; + } + + public void deleteSession(LocalSession session) { + long sessionId = session.getSessionId(); + log.log(Level.FINE, "Deleting local session " + sessionId); + LocalSessionStateWatcher watcher = sessionStateWatchers.remove(sessionId); + if (watcher != null) watcher.close(); + sessionCache.removeSession(sessionId); + NestedTransaction transaction = new NestedTransaction(); + session.delete(transaction); + transaction.commit(); + } + + public void close() { + deleteAllSessions(); + tenantFileSystemDirs.delete(); + } + + private void deleteAllSessions() { + List<LocalSession> sessions = new ArrayList<>(sessionCache.getSessions()); + for (LocalSession session : sessions) { + deleteSession(session); + } + } + + @Override + public String toString() { + return getSessions().toString(); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionStateWatcher.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionStateWatcher.java index 1067a373f41..662094fc0ca 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionStateWatcher.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionStateWatcher.java @@ -21,14 +21,14 @@ public class LocalSessionStateWatcher { private final Curator.FileCache fileCache; private final LocalSession session; - private final SessionRepository sessionRepository; + private final LocalSessionRepo localSessionRepo; private final Executor zkWatcherExecutor; LocalSessionStateWatcher(Curator.FileCache fileCache, LocalSession session, - SessionRepository sessionRepository, Executor zkWatcherExecutor) { + LocalSessionRepo localSessionRepo, Executor zkWatcherExecutor) { this.fileCache = fileCache; this.session = session; - this.sessionRepository = sessionRepository; + this.localSessionRepo = localSessionRepo; this.zkWatcherExecutor = zkWatcherExecutor; this.fileCache.start(); this.fileCache.addListener(this::nodeChanged); @@ -40,9 +40,9 @@ public class LocalSessionStateWatcher { log.log(status == Session.Status.DELETE ? Level.INFO : Level.FINE, session.logPre() + "Session change: Local session " + sessionId + " changed status to " + status); - if (status.equals(Session.Status.DELETE) && sessionRepository.getSession(sessionId) != null) { + if (status.equals(Session.Status.DELETE) && localSessionRepo.getSession(sessionId) != null) { log.log(Level.FINE, session.logPre() + "Deleting session " + sessionId); - sessionRepository.deleteSession(session); + localSessionRepo.deleteSession(session); } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java new file mode 100644 index 00000000000..0e538b05931 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java @@ -0,0 +1,241 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Multiset; +import com.yahoo.concurrent.StripedExecutor; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; + +import java.time.Clock; +import java.util.logging.Level; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.GlobalComponentRegistry; +import com.yahoo.vespa.config.server.ReloadHandler; +import com.yahoo.vespa.config.server.application.TenantApplications; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.config.server.tenant.TenantRepository; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.flags.BooleanFlag; +import com.yahoo.vespa.flags.FlagSource; +import com.yahoo.vespa.flags.Flags; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.recipes.cache.ChildData; +import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Session repository for RemoteSessions. There is one such repo per tenant. + * Will watch/prepare sessions (applications) based on watched nodes in ZooKeeper. The zookeeper state watched in + * this class is shared between all config servers, so it should not modify any global state, because the operation + * will be performed on all servers. The repo can be regarded as read only from the POV of the configserver. + * + * @author Vegard Havdal + * @author Ulf Lilleengen + */ +public class RemoteSessionRepo { + + private static final Logger log = Logger.getLogger(RemoteSessionRepo.class.getName()); + + private final Curator curator; + private final Path sessionsPath; + private final SessionFactory sessionFactory; + private final Map<Long, RemoteSessionStateWatcher> sessionStateWatchers = new HashMap<>(); + private final ReloadHandler reloadHandler; + private final TenantName tenantName; + private final MetricUpdater metrics; + private final BooleanFlag distributeApplicationPackage; + private final Curator.DirectoryCache directoryCache; + private final TenantApplications applicationRepo; + private final Executor zkWatcherExecutor; + private final SessionCache<RemoteSession> sessionCache; + + public RemoteSessionRepo(GlobalComponentRegistry componentRegistry, + SessionFactory sessionFactory, + ReloadHandler reloadHandler, + TenantName tenantName, + TenantApplications applicationRepo, + FlagSource flagSource) { + this.sessionCache = new SessionCache<>(); + this.curator = componentRegistry.getCurator(); + this.sessionsPath = TenantRepository.getSessionsPath(tenantName); + this.applicationRepo = applicationRepo; + this.sessionFactory = sessionFactory; + this.reloadHandler = reloadHandler; + this.tenantName = tenantName; + this.metrics = componentRegistry.getMetrics().getOrCreateMetricUpdater(Metrics.createDimensions(tenantName)); + this.distributeApplicationPackage = Flags.CONFIGSERVER_DISTRIBUTE_APPLICATION_PACKAGE.bindTo(flagSource); + StripedExecutor<TenantName> zkWatcherExecutor = componentRegistry.getZkWatcherExecutor(); + this.zkWatcherExecutor = command -> zkWatcherExecutor.execute(tenantName, command); + initializeSessions(); + this.directoryCache = curator.createDirectoryCache(sessionsPath.getAbsolute(), false, false, componentRegistry.getZkCacheExecutor()); + this.directoryCache.addListener(this::childEvent); + this.directoryCache.start(); + } + + public RemoteSession getSession(long sessionId) { + return sessionCache.getSession(sessionId); + } + + public List<Long> getSessions() { + return getSessionList(curator.getChildren(sessionsPath)); + } + + public void addSession(RemoteSession session) { + sessionCache.addSession(session); + metrics.incAddedSessions(); + } + + public int deleteExpiredSessions(Clock clock, Duration expiryTime) { + int deleted = 0; + for (long sessionId : getSessions()) { + RemoteSession session = sessionCache.getSession(sessionId); + if (session == null) continue; // Internal sessions not in synch with zk, continue + if (session.getStatus() == Session.Status.ACTIVATE) continue; + if (sessionHasExpired(session.getCreateTime(), expiryTime, clock)) { + log.log(Level.INFO, "Remote session " + sessionId + " for " + tenantName + " has expired, deleting it"); + session.delete(); + deleted++; + } + } + return deleted; + } + + private boolean sessionHasExpired(Instant created, Duration expiryTime, Clock clock) { + return (created.plus(expiryTime).isBefore(clock.instant())); + } + + private List<Long> getSessionListFromDirectoryCache(List<ChildData> children) { + return getSessionList(children.stream() + .map(child -> Path.fromString(child.getPath()).getName()) + .collect(Collectors.toList())); + } + + private List<Long> getSessionList(List<String> children) { + return children.stream().map(Long::parseLong).collect(Collectors.toList()); + } + + private void initializeSessions() throws NumberFormatException { + getSessions().forEach(this::sessionAdded); + } + + private synchronized void sessionsChanged() throws NumberFormatException { + List<Long> sessions = getSessionListFromDirectoryCache(directoryCache.getCurrentData()); + checkForRemovedSessions(sessions); + checkForAddedSessions(sessions); + } + + private void checkForRemovedSessions(List<Long> sessions) { + for (RemoteSession session : sessionCache.getSessions()) + if ( ! sessions.contains(session.getSessionId())) + sessionRemoved(session.getSessionId()); + } + + private void checkForAddedSessions(List<Long> sessions) { + for (Long sessionId : sessions) + if (sessionCache.getSession(sessionId) == null) + sessionAdded(sessionId); + } + + /** + * A session for which we don't have a watcher, i.e. hitherto unknown to us. + * + * @param sessionId session id for the new session + */ + private void sessionAdded(long sessionId) { + log.log(Level.FINE, () -> "Adding session to RemoteSessionRepo: " + sessionId); + RemoteSession session = sessionFactory.createRemoteSession(sessionId); + Path sessionPath = sessionsPath.append(String.valueOf(sessionId)); + Curator.FileCache fileCache = curator.createFileCache(sessionPath.append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH).getAbsolute(), false); + fileCache.addListener(this::nodeChanged); + loadSessionIfActive(session); + addSession(session); + sessionStateWatchers.put(sessionId, new RemoteSessionStateWatcher(fileCache, reloadHandler, session, metrics, zkWatcherExecutor)); + if (distributeApplicationPackage.value()) + sessionFactory.createLocalSessionUsingDistributedApplicationPackage(sessionId); + } + + private void sessionRemoved(long sessionId) { + RemoteSessionStateWatcher watcher = sessionStateWatchers.remove(sessionId); + if (watcher != null) watcher.close(); + sessionCache.removeSession(sessionId); + metrics.incRemovedSessions(); + } + + private void loadSessionIfActive(RemoteSession session) { + for (ApplicationId applicationId : applicationRepo.activeApplications()) { + if (applicationRepo.requireActiveSessionOf(applicationId) == session.getSessionId()) { + log.log(Level.FINE, () -> "Found active application for session " + session.getSessionId() + " , loading it"); + reloadHandler.reloadConfig(session.ensureApplicationLoaded()); + log.log(Level.INFO, session.logPre() + "Application activated successfully: " + applicationId + " (generation " + session.getSessionId() + ")"); + return; + } + } + } + + public synchronized void close() { + try { + if (directoryCache != null) { + directoryCache.close(); + } + } catch (Exception e) { + log.log(Level.WARNING, "Exception when closing path cache", e); + } finally { + checkForRemovedSessions(new ArrayList<>()); + } + } + + private void nodeChanged() { + zkWatcherExecutor.execute(() -> { + Multiset<Session.Status> sessionMetrics = HashMultiset.create(); + for (RemoteSession session : sessionCache.getSessions()) { + sessionMetrics.add(session.getStatus()); + } + metrics.setNewSessions(sessionMetrics.count(Session.Status.NEW)); + metrics.setPreparedSessions(sessionMetrics.count(Session.Status.PREPARE)); + metrics.setActivatedSessions(sessionMetrics.count(Session.Status.ACTIVATE)); + metrics.setDeactivatedSessions(sessionMetrics.count(Session.Status.DEACTIVATE)); + }); + } + + @SuppressWarnings("unused") + private void childEvent(CuratorFramework ignored, PathChildrenCacheEvent event) { + zkWatcherExecutor.execute(() -> { + log.log(Level.FINE, () -> "Got child event: " + event); + switch (event.getType()) { + case CHILD_ADDED: + sessionsChanged(); + synchronizeOnNew(getSessionListFromDirectoryCache(Collections.singletonList(event.getData()))); + break; + case CHILD_REMOVED: + sessionsChanged(); + break; + case CONNECTION_RECONNECTED: + sessionsChanged(); + break; + } + }); + } + + private void synchronizeOnNew(List<Long> sessionList) { + for (long sessionId : sessionList) { + RemoteSession session = sessionCache.getSession(sessionId); + if (session == null) continue; // session might have been deleted after getting session list + log.log(Level.FINE, () -> session.logPre() + "Confirming upload for session " + sessionId); + session.confirmUpload(); + } + } + +} 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 deleted file mode 100644 index c1e883758c1..00000000000 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.session; - -import com.google.common.collect.HashMultiset; -import com.google.common.collect.Multiset; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.TenantName; -import com.yahoo.path.Path; -import com.yahoo.transaction.NestedTransaction; -import com.yahoo.vespa.config.server.GlobalComponentRegistry; -import com.yahoo.vespa.config.server.ReloadHandler; -import com.yahoo.vespa.config.server.application.TenantApplications; -import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; -import com.yahoo.vespa.config.server.monitoring.MetricUpdater; -import com.yahoo.vespa.config.server.monitoring.Metrics; -import com.yahoo.vespa.config.server.tenant.TenantRepository; -import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; -import com.yahoo.vespa.curator.Curator; -import org.apache.curator.framework.CuratorFramework; -import org.apache.curator.framework.recipes.cache.ChildData; -import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; - -import java.io.File; -import java.io.FilenameFilter; -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executor; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * Session repository for config server. Stores session state in zookeeper and file system. There are two - * different session types (RemoteSession and LocalSession). - * - * @author Ulf Lilleengen - * @author hmusum - */ -public class SessionRepository { - - private static final Logger log = Logger.getLogger(SessionRepository.class.getName()); - private static final FilenameFilter sessionApplicationsFilter = (dir, name) -> name.matches("\\d+"); - - private final SessionCache<LocalSession> localSessionCache = new SessionCache<>(); - private final SessionCache<RemoteSession> remoteSessionCache = new SessionCache<>(); - private final Map<Long, LocalSessionStateWatcher> localSessionStateWatchers = new HashMap<>(); - private final Map<Long, RemoteSessionStateWatcher> remoteSessionStateWatchers = new HashMap<>(); - private final Duration sessionLifetime; - private final Clock clock; - private final Curator curator; - private final Executor zkWatcherExecutor; - private final TenantFileSystemDirs tenantFileSystemDirs; - private final ReloadHandler reloadHandler; - private final MetricUpdater metrics; - private final Curator.DirectoryCache directoryCache; - private final TenantApplications applicationRepo; - private final Path sessionsPath; - private final SessionFactory sessionFactory; - private final TenantName tenantName; - - - public SessionRepository(TenantName tenantName, GlobalComponentRegistry componentRegistry, - SessionFactory sessionFactory, TenantApplications applicationRepo, - ReloadHandler reloadHandler) { - this.tenantName = tenantName; - this.clock = componentRegistry.getClock(); - this.curator = componentRegistry.getCurator(); - this.sessionLifetime = Duration.ofSeconds(componentRegistry.getConfigserverConfig().sessionLifetime()); - this.zkWatcherExecutor = command -> componentRegistry.getZkWatcherExecutor().execute(tenantName, command); - this.tenantFileSystemDirs = new TenantFileSystemDirs(componentRegistry.getConfigServerDB(), tenantName); - this.sessionFactory = sessionFactory; - this.applicationRepo = applicationRepo; - loadLocalSessions(sessionFactory); - - this.reloadHandler = reloadHandler; - this.sessionsPath = TenantRepository.getSessionsPath(tenantName); - this.metrics = componentRegistry.getMetrics().getOrCreateMetricUpdater(Metrics.createDimensions(tenantName)); - initializeRemoteSessions(); - this.directoryCache = curator.createDirectoryCache(sessionsPath.getAbsolute(), false, false, componentRegistry.getZkCacheExecutor()); - this.directoryCache.addListener(this::childEvent); - this.directoryCache.start(); - } - - // ---------------- Local sessions ---------------------------------------------------------------- - - public synchronized void addSession(LocalSession session) { - localSessionCache.addSession(session); - Path sessionsPath = TenantRepository.getSessionsPath(session.getTenantName()); - long sessionId = session.getSessionId(); - Curator.FileCache fileCache = curator.createFileCache(sessionsPath.append(String.valueOf(sessionId)).append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH).getAbsolute(), false); - localSessionStateWatchers.put(sessionId, new LocalSessionStateWatcher(fileCache, session, this, zkWatcherExecutor)); - } - - public LocalSession getSession(long sessionId) { - return localSessionCache.getSession(sessionId); - } - - public List<LocalSession> getSessions() { - return localSessionCache.getSessions(); - } - - private void loadLocalSessions(SessionFactory sessionFactory) { - File[] sessions = tenantFileSystemDirs.sessionsPath().listFiles(sessionApplicationsFilter); - if (sessions == null) { - return; - } - for (File session : sessions) { - try { - addSession(sessionFactory.createSessionFromId(Long.parseLong(session.getName()))); - } catch (IllegalArgumentException e) { - log.log(Level.WARNING, "Could not load session '" + - session.getAbsolutePath() + "':" + e.getMessage() + ", skipping it."); - } - } - } - - public void deleteExpiredSessions(Map<ApplicationId, Long> activeSessions) { - log.log(Level.FINE, "Purging old sessions"); - try { - for (LocalSession candidate : localSessionCache.getSessions()) { - Instant createTime = candidate.getCreateTime(); - log.log(Level.FINE, "Candidate session for deletion: " + candidate.getSessionId() + ", created: " + createTime); - - // Sessions with state other than ACTIVATED - if (hasExpired(candidate) && !isActiveSession(candidate)) { - deleteSession(candidate); - } else if (createTime.plus(Duration.ofDays(1)).isBefore(clock.instant())) { - // Sessions with state ACTIVATE, but which are not actually active - ApplicationId applicationId = candidate.getApplicationId(); - Long activeSession = activeSessions.get(applicationId); - if (activeSession == null || activeSession != candidate.getSessionId()) { - deleteSession(candidate); - log.log(Level.INFO, "Deleted inactive session " + candidate.getSessionId() + " created " + - createTime + " for '" + applicationId + "'"); - } - } - } - // Make sure to catch here, to avoid executor just dying in case of issues ... - } catch (Throwable e) { - log.log(Level.WARNING, "Error when purging old sessions ", e); - } - log.log(Level.FINE, "Done purging old sessions"); - } - - private boolean hasExpired(LocalSession candidate) { - return (candidate.getCreateTime().plus(sessionLifetime).isBefore(clock.instant())); - } - - private boolean isActiveSession(LocalSession candidate) { - return candidate.getStatus() == Session.Status.ACTIVATE; - } - - public void deleteSession(LocalSession session) { - long sessionId = session.getSessionId(); - log.log(Level.FINE, "Deleting local session " + sessionId); - LocalSessionStateWatcher watcher = localSessionStateWatchers.remove(sessionId); - if (watcher != null) watcher.close(); - localSessionCache.removeSession(sessionId); - NestedTransaction transaction = new NestedTransaction(); - session.delete(transaction); - transaction.commit(); - } - - public void close() { - deleteAllSessions(); - tenantFileSystemDirs.delete(); - try { - if (directoryCache != null) { - directoryCache.close(); - } - } catch (Exception e) { - log.log(Level.WARNING, "Exception when closing path cache", e); - } finally { - checkForRemovedSessions(new ArrayList<>()); - } - } - - private void deleteAllSessions() { - List<LocalSession> sessions = new ArrayList<>(localSessionCache.getSessions()); - for (LocalSession session : sessions) { - deleteSession(session); - } - } - - // ---------------- Remote sessions ---------------------------------------------------------------- - - public RemoteSession getRemoteSession(long sessionId) { - return remoteSessionCache.getSession(sessionId); - } - - public List<Long> getRemoteSessions() { - return getRemoteSessionList(curator.getChildren(sessionsPath)); - } - - public void addRemoteSession(RemoteSession session) { - remoteSessionCache.addSession(session); - } - - public int deleteExpiredRemoteSessions(Clock clock, Duration expiryTime) { - int deleted = 0; - for (long sessionId : getRemoteSessions()) { - RemoteSession session = remoteSessionCache.getSession(sessionId); - if (session == null) continue; // Internal sessions not in synch with zk, continue - if (session.getStatus() == Session.Status.ACTIVATE) continue; - if (remoteSessionHasExpired(session.getCreateTime(), expiryTime, clock)) { - log.log(Level.INFO, "Remote session " + sessionId + " for " + tenantName + " has expired, deleting it"); - session.delete(); - deleted++; - } - } - return deleted; - } - - private boolean remoteSessionHasExpired(Instant created, Duration expiryTime, Clock clock) { - return (created.plus(expiryTime).isBefore(clock.instant())); - } - - private List<Long> getRemoteSessionListFromDirectoryCache(List<ChildData> children) { - return getRemoteSessionList(children.stream() - .map(child -> Path.fromString(child.getPath()).getName()) - .collect(Collectors.toList())); - } - - private List<Long> getRemoteSessionList(List<String> children) { - return children.stream().map(Long::parseLong).collect(Collectors.toList()); - } - - private void initializeRemoteSessions() throws NumberFormatException { - getRemoteSessions().forEach(this::sessionAdded); - } - - private synchronized void remoteSessionsChanged() throws NumberFormatException { - List<Long> sessions = getRemoteSessionListFromDirectoryCache(directoryCache.getCurrentData()); - checkForRemovedSessions(sessions); - checkForAddedSessions(sessions); - } - - private void checkForRemovedSessions(List<Long> sessions) { - for (RemoteSession session : remoteSessionCache.getSessions()) - if (!sessions.contains(session.getSessionId())) - sessionRemoved(session.getSessionId()); - } - - private void checkForAddedSessions(List<Long> sessions) { - for (Long sessionId : sessions) - if (remoteSessionCache.getSession(sessionId) == null) - sessionAdded(sessionId); - } - - /** - * A session for which we don't have a watcher, i.e. hitherto unknown to us. - * - * @param sessionId session id for the new session - */ - private void sessionAdded(long sessionId) { - log.log(Level.FINE, () -> "Adding session " + sessionId); - RemoteSession session = sessionFactory.createRemoteSession(sessionId); - Path sessionPath = sessionsPath.append(String.valueOf(sessionId)); - Curator.FileCache fileCache = curator.createFileCache(sessionPath.append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH).getAbsolute(), false); - fileCache.addListener(this::nodeChanged); - loadSessionIfActive(session); - addRemoteSession(session); - metrics.incAddedSessions(); - remoteSessionStateWatchers.put(sessionId, new RemoteSessionStateWatcher(fileCache, reloadHandler, session, metrics, zkWatcherExecutor)); - } - - private void sessionRemoved(long sessionId) { - RemoteSessionStateWatcher watcher = remoteSessionStateWatchers.remove(sessionId); - if (watcher != null) watcher.close(); - remoteSessionCache.removeSession(sessionId); - metrics.incRemovedSessions(); - } - - private void loadSessionIfActive(RemoteSession session) { - for (ApplicationId applicationId : applicationRepo.activeApplications()) { - if (applicationRepo.requireActiveSessionOf(applicationId) == session.getSessionId()) { - log.log(Level.FINE, () -> "Found active application for session " + session.getSessionId() + " , loading it"); - reloadHandler.reloadConfig(session.ensureApplicationLoaded()); - log.log(Level.INFO, session.logPre() + "Application activated successfully: " + applicationId + " (generation " + session.getSessionId() + ")"); - return; - } - } - } - - private void nodeChanged() { - zkWatcherExecutor.execute(() -> { - Multiset<Session.Status> sessionMetrics = HashMultiset.create(); - for (RemoteSession session : remoteSessionCache.getSessions()) { - sessionMetrics.add(session.getStatus()); - } - metrics.setNewSessions(sessionMetrics.count(Session.Status.NEW)); - metrics.setPreparedSessions(sessionMetrics.count(Session.Status.PREPARE)); - metrics.setActivatedSessions(sessionMetrics.count(Session.Status.ACTIVATE)); - metrics.setDeactivatedSessions(sessionMetrics.count(Session.Status.DEACTIVATE)); - }); - } - - @SuppressWarnings("unused") - private void childEvent(CuratorFramework ignored, PathChildrenCacheEvent event) { - zkWatcherExecutor.execute(() -> { - log.log(Level.FINE, () -> "Got child event: " + event); - switch (event.getType()) { - case CHILD_ADDED: - remoteSessionsChanged(); - synchronizeOnNew(getRemoteSessionListFromDirectoryCache(Collections.singletonList(event.getData()))); - break; - case CHILD_REMOVED: - remoteSessionsChanged(); - break; - case CONNECTION_RECONNECTED: - remoteSessionsChanged(); - break; - } - }); - } - - private void synchronizeOnNew(List<Long> sessionList) { - for (long sessionId : sessionList) { - RemoteSession session = remoteSessionCache.getSession(sessionId); - if (session == null) continue; // session might have been deleted after getting session list - log.log(Level.FINE, () -> session.logPre() + "Confirming upload for session " + sessionId); - session.confirmUpload(); - } - } - - @Override - public String toString() { - return getSessions().toString(); - } - -} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/Tenant.java b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/Tenant.java index 0a539db4e43..f0aab8b2312 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/Tenant.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/Tenant.java @@ -6,8 +6,9 @@ import com.yahoo.path.Path; import com.yahoo.vespa.config.server.ReloadHandler; import com.yahoo.vespa.config.server.RequestHandler; import com.yahoo.vespa.config.server.application.TenantApplications; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.RemoteSessionRepo; import com.yahoo.vespa.config.server.session.SessionFactory; -import com.yahoo.vespa.config.server.session.SessionRepository; import com.yahoo.vespa.curator.Curator; import org.apache.zookeeper.data.Stat; @@ -27,9 +28,10 @@ public class Tenant implements TenantHandlerProvider { static final String APPLICATIONS = "applications"; private final TenantName name; + private final RemoteSessionRepo remoteSessionRepo; private final Path path; private final SessionFactory sessionFactory; - private final SessionRepository sessionRepository; + private final LocalSessionRepo localSessionRepo; private final TenantApplications applicationRepo; private final RequestHandler requestHandler; private final ReloadHandler reloadHandler; @@ -37,7 +39,8 @@ public class Tenant implements TenantHandlerProvider { Tenant(TenantName name, SessionFactory sessionFactory, - SessionRepository sessionRepository, + LocalSessionRepo localSessionRepo, + RemoteSessionRepo remoteSessionRepo, RequestHandler requestHandler, ReloadHandler reloadHandler, TenantApplications applicationRepo, @@ -46,8 +49,9 @@ public class Tenant implements TenantHandlerProvider { this.path = TenantRepository.getTenantPath(name); this.requestHandler = requestHandler; this.reloadHandler = reloadHandler; + this.remoteSessionRepo = remoteSessionRepo; this.sessionFactory = sessionFactory; - this.sessionRepository = sessionRepository; + this.localSessionRepo = localSessionRepo; this.applicationRepo = applicationRepo; this.curator = curator; } @@ -70,8 +74,13 @@ public class Tenant implements TenantHandlerProvider { return requestHandler; } - public SessionRepository getSessionRepo() { - return sessionRepository; + /** + * The RemoteSessionRepo for this + * + * @return repo + */ + public RemoteSessionRepo getRemoteSessionRepo() { + return remoteSessionRepo; } public TenantName getName() { @@ -86,8 +95,8 @@ public class Tenant implements TenantHandlerProvider { return sessionFactory; } - public SessionRepository getSessionRepository() { - return sessionRepository; + public LocalSessionRepo getLocalSessionRepo() { + return localSessionRepo; } @Override @@ -131,9 +140,9 @@ public class Tenant implements TenantHandlerProvider { * Called by watchers as a reaction to {@link #delete()}. */ void close() { - sessionRepository.close(); // Closes watchers and clears memory. + remoteSessionRepo.close(); // Closes watchers and clears memory. applicationRepo.close(); // Closes watchers. - sessionRepository.close(); // Closes watchers, clears memory, and deletes local files and ZK session state. + localSessionRepo.close(); // Closes watchers, clears memory, and deletes local files and ZK session state. } /** Deletes the tenant tree from ZooKeeper (application and session status for the tenant) and triggers {@link #close()}. */ diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/TenantRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/TenantRepository.java index 841407817c6..d34f89a179b 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/TenantRepository.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/TenantRepository.java @@ -14,7 +14,8 @@ import com.yahoo.vespa.config.server.RequestHandler; import com.yahoo.vespa.config.server.application.TenantApplications; import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; import com.yahoo.vespa.config.server.monitoring.MetricUpdater; -import com.yahoo.vespa.config.server.session.SessionRepository; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.RemoteSessionRepo; import com.yahoo.vespa.config.server.session.SessionFactory; import com.yahoo.vespa.curator.Curator; import org.apache.curator.framework.CuratorFramework; @@ -48,7 +49,7 @@ import java.util.stream.Collectors; * This component will monitor the set of tenants in the config server by watching in ZooKeeper. * It will set up Tenant objects accordingly, which will manage the config sessions per tenant. * This class will read the preexisting set of tenants from ZooKeeper at startup. (For now it will also - * create a default tenant since that will be used for APIs that do no know about tenants or have not yet + * create a default tenant since that will be used for API that do no know about tenants or have not yet * implemented support for it). * * This instance is called from two different threads, the http handler threads and the zookeeper watcher threads. @@ -223,10 +224,15 @@ public class TenantRepository { if (reloadHandler == null) reloadHandler = applicationRepo; SessionFactory sessionFactory = new SessionFactory(componentRegistry, applicationRepo, applicationRepo, tenantName); - SessionRepository sessionRepository = new SessionRepository(tenantName, componentRegistry, sessionFactory, - applicationRepo, reloadHandler); + LocalSessionRepo localSessionRepo = new LocalSessionRepo(tenantName, componentRegistry, sessionFactory); + RemoteSessionRepo remoteSessionRepo = new RemoteSessionRepo(componentRegistry, + sessionFactory, + reloadHandler, + tenantName, + applicationRepo, + componentRegistry.getFlagSource()); log.log(Level.INFO, "Creating tenant '" + tenantName + "'"); - Tenant tenant = new Tenant(tenantName, sessionFactory, sessionRepository, requestHandler, + Tenant tenant = new Tenant(tenantName, sessionFactory, localSessionRepo, remoteSessionRepo, requestHandler, reloadHandler, applicationRepo, componentRegistry.getCurator()); notifyNewTenant(tenant); tenants.putIfAbsent(tenantName, tenant); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java index ab75f64c92c..a3d072be38b 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java @@ -37,7 +37,7 @@ import com.yahoo.vespa.config.server.http.InternalServerException; import com.yahoo.vespa.config.server.http.SessionHandlerTest; import com.yahoo.vespa.config.server.http.v2.PrepareResult; import com.yahoo.vespa.config.server.session.LocalSession; -import com.yahoo.vespa.config.server.session.SessionRepository; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; import com.yahoo.vespa.config.server.session.PrepareParams; import com.yahoo.vespa.config.server.session.RemoteSession; import com.yahoo.vespa.config.server.session.Session; @@ -146,7 +146,7 @@ public class ApplicationRepositoryTest { TenantName tenantName = applicationId().tenant(); Tenant tenant = tenantRepository.getTenant(tenantName); - LocalSession session = tenant.getSessionRepository().getSession(tenant.getApplicationRepo() + LocalSession session = tenant.getLocalSessionRepo().getSession(tenant.getApplicationRepo() .requireActiveSessionOf(applicationId())); session.getAllocatedHosts(); } @@ -178,7 +178,7 @@ public class ApplicationRepositoryTest { TenantName tenantName = applicationId().tenant(); Tenant tenant = tenantRepository.getTenant(tenantName); - LocalSession session = tenant.getSessionRepository().getSession( + LocalSession session = tenant.getLocalSessionRepo().getSession( tenant.getApplicationRepo().requireActiveSessionOf(applicationId())); assertEquals(firstSessionId, session.getMetaData().getPreviousActiveGeneration()); } @@ -295,17 +295,17 @@ public class ApplicationRepositoryTest { PrepareResult result = deployApp(testApp); long sessionId = result.sessionId(); Tenant tenant = tenantRepository.getTenant(applicationId().tenant()); - LocalSession applicationData = tenant.getSessionRepository().getSession(sessionId); + LocalSession applicationData = tenant.getLocalSessionRepo().getSession(sessionId); assertNotNull(applicationData); assertNotNull(applicationData.getApplicationId()); - assertNotNull(tenant.getSessionRepo().getRemoteSession(sessionId)); + assertNotNull(tenant.getRemoteSessionRepo().getSession(sessionId)); assertNotNull(applicationRepository.getActiveSession(applicationId())); // Delete app and verify that it has been deleted from repos and provisioner assertTrue(applicationRepository.delete(applicationId())); assertNull(applicationRepository.getActiveSession(applicationId())); - assertNull(tenant.getSessionRepository().getSession(sessionId)); - assertNull(tenant.getSessionRepo().getRemoteSession(sessionId)); + assertNull(tenant.getLocalSessionRepo().getSession(sessionId)); + assertNull(tenant.getRemoteSessionRepo().getSession(sessionId)); assertTrue(provisioner.removed); assertEquals(tenant.getName(), provisioner.lastApplicationId.tenant()); assertEquals(applicationId(), provisioner.lastApplicationId); @@ -346,7 +346,7 @@ public class ApplicationRepositoryTest { RemoteSession activeSession = applicationRepository.getActiveSession(applicationId()); assertNull(activeSession); Tenant tenant = tenantRepository.getTenant(applicationId().tenant()); - assertNull(tenant.getSessionRepo().getRemoteSession(prepareResult.sessionId())); + assertNull(tenant.getRemoteSessionRepo().getSession(prepareResult.sessionId())); assertTrue(applicationRepository.delete(applicationId())); } @@ -379,14 +379,14 @@ public class ApplicationRepositoryTest { assertNotEquals(activeSessionId, deployment3session); // No change to active session id assertEquals(activeSessionId, tester.tenant().getApplicationRepo().requireActiveSessionOf(tester.applicationId())); - SessionRepository sessionRepository = tester.tenant().getSessionRepository(); - assertEquals(3, sessionRepository.getSessions().size()); + LocalSessionRepo localSessionRepo = tester.tenant().getLocalSessionRepo(); + assertEquals(3, localSessionRepo.getSessions().size()); clock.advance(Duration.ofHours(1)); // longer than session lifetime // All sessions except 3 should be removed after the call to deleteExpiredLocalSessions tester.applicationRepository().deleteExpiredLocalSessions(); - Collection<LocalSession> sessions = sessionRepository.getSessions(); + Collection<LocalSession> sessions = localSessionRepo.getSessions(); assertEquals(1, sessions.size()); ArrayList<LocalSession> localSessions = new ArrayList<>(sessions); LocalSession localSession = localSessions.get(0); @@ -400,9 +400,9 @@ public class ApplicationRepositoryTest { assertTrue(deployment4.isPresent()); deployment4.get().prepare(); // session 5 (not activated) - assertEquals(2, sessionRepository.getSessions().size()); - sessionRepository.deleteSession(localSession); - assertEquals(1, sessionRepository.getSessions().size()); + assertEquals(2, localSessionRepo.getSessions().size()); + localSessionRepo.deleteSession(localSession); + assertEquals(1, localSessionRepo.getSessions().size()); // Check that trying to expire when there are no active sessions works tester.applicationRepository().deleteExpiredLocalSessions(); @@ -457,7 +457,7 @@ public class ApplicationRepositoryTest { TenantName tenantName = applicationId().tenant(); Tenant tenant = tenantRepository.getTenant(tenantName); - LocalSession session = tenant.getSessionRepository().getSession(tenant.getApplicationRepo().requireActiveSessionOf(applicationId())); + LocalSession session = tenant.getLocalSessionRepo().getSession(tenant.getApplicationRepo().requireActiveSessionOf(applicationId())); List<NetworkPorts.Allocation> list = new ArrayList<>(); list.add(new NetworkPorts.Allocation(8080, "container", "container/container.0", "http")); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/DeployTester.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/DeployTester.java index bc9f45698e9..b2091d6e537 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/DeployTester.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/DeployTester.java @@ -249,7 +249,7 @@ public class DeployTester { public AllocatedHosts getAllocatedHostsOf(ApplicationId applicationId) { Tenant tenant = tenant(); - LocalSession session = tenant.getSessionRepository().getSession(tenant.getApplicationRepo() + LocalSession session = tenant.getLocalSessionRepo().getSession(tenant.getApplicationRepo() .requireActiveSessionOf(applicationId)); return session.getAllocatedHosts(); } diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java index 156200a061c..36467a2ca64 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java @@ -33,11 +33,11 @@ public class RedeployTest { assertTrue(deployment.isPresent()); long activeSessionIdBefore = tester.applicationRepository().getActiveSession(tester.applicationId()).getSessionId(); - assertEquals(tester.applicationId(), tester.tenant().getSessionRepository().getSession(activeSessionIdBefore).getApplicationId()); + assertEquals(tester.applicationId(), tester.tenant().getLocalSessionRepo().getSession(activeSessionIdBefore).getApplicationId()); deployment.get().activate(); long activeSessionIdAfter = tester.applicationRepository().getActiveSession(tester.applicationId()).getSessionId(); assertEquals(activeSessionIdAfter, activeSessionIdBefore + 1); - assertEquals(tester.applicationId(), tester.tenant().getSessionRepository().getSession(activeSessionIdAfter).getApplicationId()); + assertEquals(tester.applicationId(), tester.tenant().getLocalSessionRepo().getSession(activeSessionIdAfter).getApplicationId()); } /** No deployment is done because there is no local active session. */ diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java index 4cf81d22e3c..078dc47af51 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java @@ -54,13 +54,13 @@ public class ApplicationContentHandlerTest extends ContentHandlerTestBase { session2 = new MockLocalSession(2, FilesApplicationPackage.fromFile(new File("src/test/apps/content"))); Tenant tenant1 = tenantRepository.getTenant(tenantName1); - tenant1.getSessionRepository().addSession(session2); + tenant1.getLocalSessionRepo().addSession(session2); tenant1.getApplicationRepo().createApplication(idTenant1); tenant1.getApplicationRepo().createPutTransaction(idTenant1, 2).commit(); MockLocalSession session3 = new MockLocalSession(3, FilesApplicationPackage.fromFile(new File("src/test/apps/content2"))); Tenant tenant2 = tenantRepository.getTenant(tenantName2); - tenant2.getSessionRepository().addSession(session3); + tenant2.getLocalSessionRepo().addSession(session3); tenant2.getApplicationRepo().createApplication(idTenant2); tenant2.getApplicationRepo().createPutTransaction(idTenant2, 3).commit(); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java index ec141a61ee6..ff8f7a291ad 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java @@ -286,13 +286,13 @@ public class ApplicationHandlerTest { private void deleteAndAssertOKResponseMocked(ApplicationId applicationId, boolean fullAppIdInUrl) throws IOException { long sessionId = tenantRepository.getTenant(applicationId.tenant()).getApplicationRepo().requireActiveSessionOf(applicationId); deleteAndAssertResponse(applicationId, Zone.defaultZone(), Response.Status.OK, null, fullAppIdInUrl); - assertNull(tenantRepository.getTenant(applicationId.tenant()).getSessionRepository().getSession(sessionId)); + assertNull(tenantRepository.getTenant(applicationId.tenant()).getLocalSessionRepo().getSession(sessionId)); } private void deleteAndAssertOKResponse(Tenant tenant, ApplicationId applicationId) throws IOException { long sessionId = tenant.getApplicationRepo().requireActiveSessionOf(applicationId); deleteAndAssertResponse(applicationId, Zone.defaultZone(), Response.Status.OK, null, true); - assertNull(tenant.getSessionRepository().getSession(sessionId)); + assertNull(tenant.getLocalSessionRepo().getSession(sessionId)); } private void deleteAndAssertResponse(ApplicationId applicationId, Zone zone, int expectedStatus, HttpErrorResponse.errorCodes errorCode, boolean fullAppIdInUrl) throws IOException { diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java index 457acf8c376..2eaa5d75ba7 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java @@ -52,11 +52,11 @@ public class HostHandlerTest { tenant.getApplicationRepo().createApplication(applicationId); tenant.getApplicationRepo().createPutTransaction(applicationId, sessionId).commit(); ApplicationPackage app = FilesApplicationPackage.fromFile(testApp); - tenant.getSessionRepository().addSession(new SessionHandlerTest.MockLocalSession(sessionId, app, applicationId)); + tenant.getLocalSessionRepo().addSession(new SessionHandlerTest.MockLocalSession(sessionId, app, applicationId)); TestComponentRegistry componentRegistry = new TestComponentRegistry.Builder() .modelFactoryRegistry(new ModelFactoryRegistry(Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry())))) .build(); - tenant.getSessionRepo().addRemoteSession(new RemoteSession(tenant.getName(), sessionId, componentRegistry, new MockSessionZKClient(app))); + tenant.getRemoteSessionRepo().addSession(new RemoteSession(tenant.getName(), sessionId, componentRegistry, new MockSessionZKClient(app))); } @Before diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java index f639843ac08..88bf6fb7172 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java @@ -43,7 +43,7 @@ public class SessionContentHandlerTest extends ContentHandlerTestBase { public void setupHandler() throws Exception { tenantRepository = new TenantRepository(componentRegistry, false); tenantRepository.addTenant(tenant); - tenantRepository.getTenant(tenant).getSessionRepository().addSession(new MockLocalSession(1L, FilesApplicationPackage.fromFile(createTestApp()))); + tenantRepository.getTenant(tenant).getLocalSessionRepo().addSession(new MockLocalSession(1L, FilesApplicationPackage.fromFile(createTestApp()))); handler = createHandler(); pathPrefix = "/application/v2/tenant/" + tenant + "/session/"; baseUrl = "http://foo:1337/application/v2/tenant/" + tenant + "/session/1/content/"; diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionRepoTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionRepoTest.java new file mode 100644 index 00000000000..a758698d3b5 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionRepoTest.java @@ -0,0 +1,106 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.TenantName; +import com.yahoo.io.IOUtils; +import com.yahoo.vespa.config.server.GlobalComponentRegistry; +import com.yahoo.vespa.config.server.MockReloadHandler; +import com.yahoo.vespa.config.server.TestComponentRegistry; +import com.yahoo.vespa.config.server.application.TenantApplications; +import com.yahoo.vespa.config.server.host.HostRegistry; +import com.yahoo.vespa.config.server.http.SessionHandlerTest; +import com.yahoo.vespa.curator.mock.MockCurator; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * @author Ulf Lilleengen + */ +public class LocalSessionRepoTest { + + private File testApp = new File("src/test/apps/app"); + private LocalSessionRepo repo; + private static final TenantName tenantName = TenantName.defaultName(); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void setupSessions() throws Exception { + setupSessions(tenantName, true); + } + + private void setupSessions(TenantName tenantName, boolean createInitialSessions) throws Exception { + File configserverDbDir = temporaryFolder.newFolder().getAbsoluteFile(); + if (createInitialSessions) { + Path sessionsPath = Paths.get(configserverDbDir.getAbsolutePath(), "tenants", tenantName.value(), "sessions"); + IOUtils.copyDirectory(testApp, sessionsPath.resolve("1").toFile()); + IOUtils.copyDirectory(testApp, sessionsPath.resolve("2").toFile()); + IOUtils.copyDirectory(testApp, sessionsPath.resolve("3").toFile()); + } + GlobalComponentRegistry globalComponentRegistry = new TestComponentRegistry.Builder() + .curator(new MockCurator()) + .configServerConfig(new ConfigserverConfig.Builder() + .configServerDBDir(configserverDbDir.getAbsolutePath()) + .configDefinitionsDir(temporaryFolder.newFolder().getAbsolutePath()) + .sessionLifetime(5) + .build()) + .build(); + SessionFactory sessionFactory = new SessionFactory(globalComponentRegistry, + TenantApplications.create(globalComponentRegistry, tenantName), + new HostRegistry<>(), + tenantName); + repo = new LocalSessionRepo(tenantName, globalComponentRegistry, sessionFactory); + } + + @Test + public void require_that_sessions_can_be_loaded_from_disk() { + assertNotNull(repo.getSession(1L)); + assertNotNull(repo.getSession(2L)); + assertNotNull(repo.getSession(3L)); + assertNull(repo.getSession(4L)); + } + + @Test + public void require_that_all_sessions_are_deleted() { + repo.close(); + assertNull(repo.getSession(1L)); + assertNull(repo.getSession(2L)); + assertNull(repo.getSession(3L)); + } + + @Test + public void require_that_sessions_belong_to_a_tenant() { + // tenant is "default" + assertNotNull(repo.getSession(1L)); + assertNotNull(repo.getSession(2L)); + assertNotNull(repo.getSession(3L)); + assertNull(repo.getSession(4L)); + + // tenant is "newTenant" + try { + setupSessions(TenantName.from("newTenant"), false); + } catch (Exception e) { + fail(); + } + assertNull(repo.getSession(1L)); + + repo.addSession(new SessionHandlerTest.MockLocalSession(1L, FilesApplicationPackage.fromFile(testApp))); + repo.addSession(new SessionHandlerTest.MockLocalSession(2L, FilesApplicationPackage.fromFile(testApp))); + assertNotNull(repo.getSession(1L)); + assertNotNull(repo.getSession(2L)); + assertNull(repo.getSession(3L)); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionRepoTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionRepoTest.java new file mode 100644 index 00000000000..468dd5a15a7 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionRepoTest.java @@ -0,0 +1,145 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.path.Path; +import com.yahoo.text.Utf8; +import com.yahoo.vespa.config.server.TestComponentRegistry; +import com.yahoo.vespa.config.server.tenant.Tenant; +import com.yahoo.vespa.config.server.tenant.TenantRepository; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.mock.MockCurator; +import org.junit.Before; +import org.junit.Test; + +import java.time.Duration; +import java.time.Instant; +import java.util.function.LongPredicate; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +/** + * @author Ulf Lilleengen + */ +public class RemoteSessionRepoTest { + + private static final TenantName tenantName = TenantName.defaultName(); + + private RemoteSessionRepo remoteSessionRepo; + private Curator curator; + TenantRepository tenantRepository; + + @Before + public void setupFacade() { + curator = new MockCurator(); + TestComponentRegistry componentRegistry = new TestComponentRegistry.Builder() + .curator(curator) + .build(); + tenantRepository = new TenantRepository(componentRegistry, false); + tenantRepository.addTenant(tenantName); + this.remoteSessionRepo = tenantRepository.getTenant(tenantName).getRemoteSessionRepo(); + curator.create(TenantRepository.getTenantPath(tenantName).append("/applications")); + curator.create(TenantRepository.getSessionsPath(tenantName)); + createSession(1L, false); + createSession(2L, false); + } + + private void createSession(long sessionId, boolean wait) { + createSession(sessionId, wait, tenantName); + } + + private void createSession(long sessionId, boolean wait, TenantName tenantName) { + Path sessionsPath = TenantRepository.getSessionsPath(tenantName); + SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, sessionsPath.append(String.valueOf(sessionId))); + zkc.createNewSession(Instant.now()); + if (wait) { + Curator.CompletionWaiter waiter = zkc.getUploadWaiter(); + waiter.awaitCompletion(Duration.ofSeconds(120)); + } + } + + @Test + public void testInitialize() { + assertSessionExists(1L); + assertSessionExists(2L); + } + + @Test + public void testCreateSession() { + createSession(3L, true); + assertSessionExists(3L); + } + + @Test + public void testSessionStateChange() throws Exception { + long sessionId = 3L; + createSession(sessionId, true); + assertSessionStatus(sessionId, Session.Status.NEW); + assertStatusChange(sessionId, Session.Status.PREPARE); + assertStatusChange(sessionId, Session.Status.ACTIVATE); + + Path session = TenantRepository.getSessionsPath(tenantName).append("" + sessionId); + curator.delete(session); + assertSessionRemoved(sessionId); + assertNull(remoteSessionRepo.getSession(sessionId)); + } + + // If reading a session throws an exception it should be handled and not prevent other applications + // from loading. In this test we just show that we end up with one session in remote session + // repo even if it had bad data (by making getSessionIdForApplication() in FailingTenantApplications + // throw an exception). + @Test + public void testBadApplicationRepoOnActivate() { + long sessionId = 3L; + TenantName mytenant = TenantName.from("mytenant"); + curator.set(TenantRepository.getApplicationsPath(mytenant).append("mytenant:appX:default"), new byte[0]); // Invalid data + tenantRepository.addTenant(mytenant); + Tenant tenant = tenantRepository.getTenant(mytenant); + curator.create(TenantRepository.getSessionsPath(mytenant)); + remoteSessionRepo = tenant.getRemoteSessionRepo(); + assertThat(remoteSessionRepo.getSessions().size(), is(0)); + createSession(sessionId, true, mytenant); + assertThat(remoteSessionRepo.getSessions().size(), is(1)); + } + + private void assertStatusChange(long sessionId, Session.Status status) throws Exception { + Path statePath = TenantRepository.getSessionsPath(tenantName).append("" + sessionId).append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH); + curator.create(statePath); + curator.framework().setData().forPath(statePath.getAbsolute(), Utf8.toBytes(status.toString())); + assertSessionStatus(sessionId, status); + } + + private void assertSessionRemoved(long sessionId) { + waitFor(p -> remoteSessionRepo.getSession(sessionId) == null, sessionId); + assertNull(remoteSessionRepo.getSession(sessionId)); + } + + private void assertSessionExists(long sessionId) { + assertSessionStatus(sessionId, Session.Status.NEW); + } + + private void assertSessionStatus(long sessionId, Session.Status status) { + waitFor(p -> remoteSessionRepo.getSession(sessionId) != null && + remoteSessionRepo.getSession(sessionId).getStatus() == status, sessionId); + assertNotNull(remoteSessionRepo.getSession(sessionId)); + assertThat(remoteSessionRepo.getSession(sessionId).getStatus(), is(status)); + } + + private void waitFor(LongPredicate predicate, long sessionId) { + long endTime = System.currentTimeMillis() + 60_000; + boolean ok; + do { + ok = predicate.test(sessionId); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } while (System.currentTimeMillis() < endTime && !ok); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepositoryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepositoryTest.java deleted file mode 100644 index dbd71bed581..00000000000 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepositoryTest.java +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.session; - -import com.yahoo.cloud.config.ConfigserverConfig; -import com.yahoo.config.model.application.provider.FilesApplicationPackage; -import com.yahoo.config.provision.TenantName; -import com.yahoo.io.IOUtils; -import com.yahoo.text.Utf8; -import com.yahoo.vespa.config.server.GlobalComponentRegistry; -import com.yahoo.vespa.config.server.TestComponentRegistry; -import com.yahoo.vespa.config.server.application.TenantApplications; -import com.yahoo.vespa.config.server.host.HostRegistry; -import com.yahoo.vespa.config.server.http.SessionHandlerTest; -import com.yahoo.vespa.config.server.tenant.Tenant; -import com.yahoo.vespa.config.server.tenant.TenantRepository; -import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; -import com.yahoo.vespa.curator.Curator; -import com.yahoo.vespa.curator.mock.MockCurator; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.time.Instant; -import java.util.function.LongPredicate; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - -/** - * @author Ulf Lilleengen - */ -public class SessionRepositoryTest { - - private File testApp = new File("src/test/apps/app"); - private MockCurator curator; - private SessionRepository sessionRepository; - private TenantRepository tenantRepository; - private static final TenantName tenantName = TenantName.defaultName(); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Before - public void setupSessions() throws Exception { - setupSessions(tenantName, true); - } - - private void setupSessions(TenantName tenantName, boolean createInitialSessions) throws Exception { - File configserverDbDir = temporaryFolder.newFolder().getAbsoluteFile(); - if (createInitialSessions) { - Path sessionsPath = Paths.get(configserverDbDir.getAbsolutePath(), "tenants", tenantName.value(), "sessions"); - IOUtils.copyDirectory(testApp, sessionsPath.resolve("1").toFile()); - IOUtils.copyDirectory(testApp, sessionsPath.resolve("2").toFile()); - IOUtils.copyDirectory(testApp, sessionsPath.resolve("3").toFile()); - } - curator = new MockCurator(); - curator.create(TenantRepository.getTenantPath(tenantName).append("/applications")); - curator.create(TenantRepository.getSessionsPath(tenantName)); - GlobalComponentRegistry globalComponentRegistry = new TestComponentRegistry.Builder() - .curator(curator) - .configServerConfig(new ConfigserverConfig.Builder() - .configServerDBDir(configserverDbDir.getAbsolutePath()) - .configDefinitionsDir(temporaryFolder.newFolder().getAbsolutePath()) - .sessionLifetime(5) - .build()) - .build(); - tenantRepository = new TenantRepository(globalComponentRegistry, false); - TenantApplications applicationRepo = TenantApplications.create(globalComponentRegistry, tenantName); - SessionFactory sessionFactory = new SessionFactory(globalComponentRegistry, - applicationRepo, - new HostRegistry<>(), - tenantName); - sessionRepository = new SessionRepository(tenantName, globalComponentRegistry, sessionFactory, - applicationRepo, applicationRepo); - } - - @Test - public void require_that_sessions_can_be_loaded_from_disk() { - assertNotNull(sessionRepository.getSession(1L)); - assertNotNull(sessionRepository.getSession(2L)); - assertNotNull(sessionRepository.getSession(3L)); - assertNull(sessionRepository.getSession(4L)); - } - - @Test - public void require_that_all_sessions_are_deleted() { - sessionRepository.close(); - assertNull(sessionRepository.getSession(1L)); - assertNull(sessionRepository.getSession(2L)); - assertNull(sessionRepository.getSession(3L)); - } - - @Test - public void require_that_sessions_belong_to_a_tenant() { - // tenant is "default" - assertNotNull(sessionRepository.getSession(1L)); - assertNotNull(sessionRepository.getSession(2L)); - assertNotNull(sessionRepository.getSession(3L)); - assertNull(sessionRepository.getSession(4L)); - - // tenant is "newTenant" - try { - setupSessions(TenantName.from("newTenant"), false); - } catch (Exception e) { - fail(); - } - assertNull(sessionRepository.getSession(1L)); - - sessionRepository.addSession(new SessionHandlerTest.MockLocalSession(1L, FilesApplicationPackage.fromFile(testApp))); - sessionRepository.addSession(new SessionHandlerTest.MockLocalSession(2L, FilesApplicationPackage.fromFile(testApp))); - assertNotNull(sessionRepository.getSession(1L)); - assertNotNull(sessionRepository.getSession(2L)); - assertNull(sessionRepository.getSession(3L)); - } - - - private void createSession(long sessionId, boolean wait) { - createSession(sessionId, wait, tenantName); - } - - private void createSession(long sessionId, boolean wait, TenantName tenantName) { - com.yahoo.path.Path sessionsPath = TenantRepository.getSessionsPath(tenantName); - SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, sessionsPath.append(String.valueOf(sessionId))); - zkc.createNewSession(Instant.now()); - if (wait) { - Curator.CompletionWaiter waiter = zkc.getUploadWaiter(); - waiter.awaitCompletion(Duration.ofSeconds(120)); - } - } - - @Test - public void testInitialize() { - createSession(10L, false); - createSession(11L, false); - assertSessionExists(10L); - assertSessionExists(11L); - } - - @Test - public void testCreateSession() { - createSession(12L, true); - assertSessionExists(12L); - } - - @Test - public void testSessionStateChange() throws Exception { - long sessionId = 3L; - createSession(sessionId, true); - assertSessionStatus(sessionId, Session.Status.NEW); - assertStatusChange(sessionId, Session.Status.PREPARE); - assertStatusChange(sessionId, Session.Status.ACTIVATE); - - com.yahoo.path.Path session = TenantRepository.getSessionsPath(tenantName).append("" + sessionId); - curator.delete(session); - assertSessionRemoved(sessionId); - assertNull(sessionRepository.getRemoteSession(sessionId)); - } - - // If reading a session throws an exception it should be handled and not prevent other applications - // from loading. In this test we just show that we end up with one session in remote session - // repo even if it had bad data (by making getSessionIdForApplication() in FailingTenantApplications - // throw an exception). - @Test - public void testBadApplicationRepoOnActivate() { - long sessionId = 3L; - TenantName mytenant = TenantName.from("mytenant"); - curator.set(TenantRepository.getApplicationsPath(mytenant).append("mytenant:appX:default"), new byte[0]); // Invalid data - tenantRepository.addTenant(mytenant); - Tenant tenant = tenantRepository.getTenant(mytenant); - curator.create(TenantRepository.getSessionsPath(mytenant)); - sessionRepository = tenant.getSessionRepo(); - assertThat(sessionRepository.getRemoteSessions().size(), is(0)); - createSession(sessionId, true, mytenant); - assertThat(sessionRepository.getRemoteSessions().size(), is(1)); - } - - private void assertStatusChange(long sessionId, Session.Status status) throws Exception { - com.yahoo.path.Path statePath = TenantRepository.getSessionsPath(tenantName).append("" + sessionId).append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH); - curator.create(statePath); - curator.framework().setData().forPath(statePath.getAbsolute(), Utf8.toBytes(status.toString())); - assertSessionStatus(sessionId, status); - } - - private void assertSessionRemoved(long sessionId) { - waitFor(p -> sessionRepository.getRemoteSession(sessionId) == null, sessionId); - assertNull(sessionRepository.getRemoteSession(sessionId)); - } - - private void assertSessionExists(long sessionId) { - assertSessionStatus(sessionId, Session.Status.NEW); - } - - private void assertSessionStatus(long sessionId, Session.Status status) { - waitFor(p -> sessionRepository.getRemoteSession(sessionId) != null && - sessionRepository.getRemoteSession(sessionId).getStatus() == status, sessionId); - assertNotNull(sessionRepository.getRemoteSession(sessionId)); - assertThat(sessionRepository.getRemoteSession(sessionId).getStatus(), is(status)); - } - - private void waitFor(LongPredicate predicate, long sessionId) { - long endTime = System.currentTimeMillis() + 60_000; - boolean ok; - do { - ok = predicate.test(sessionId); - try { - Thread.sleep(10); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } while (System.currentTimeMillis() < endTime && !ok); - } - -} diff --git a/container-core/src/main/java/com/yahoo/container/protect/ProcessTerminator.java b/container-core/src/main/java/com/yahoo/container/protect/ProcessTerminator.java index 16cf741813c..38f5b72336b 100644 --- a/container-core/src/main/java/com/yahoo/container/protect/ProcessTerminator.java +++ b/container-core/src/main/java/com/yahoo/container/protect/ProcessTerminator.java @@ -5,7 +5,7 @@ import com.yahoo.protect.Process; /** * An injectable terminator of the Java vm. - * Components that encounters conditions where the vm should be terminated should + * Components that encounters conditions where the vm should be terminator should * request an instance of this injected. That makes termination testable * as tests can create subclasses of this which register the termination request * rather than terminating. |