/* * Copyright 2013 NGDATA nv * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.lilyproject.repository.model.impl; import com.google.common.collect.Sets; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.data.Stat; import org.lilyproject.repository.model.api.RepositoryDefinition; import org.lilyproject.repository.model.api.RepositoryExistsException; import org.lilyproject.repository.model.api.RepositoryModel; import org.lilyproject.repository.model.api.RepositoryModelEvent; import org.lilyproject.repository.model.api.RepositoryModelEventType; import org.lilyproject.repository.model.api.RepositoryModelException; import org.lilyproject.repository.model.api.RepositoryModelListener; import org.lilyproject.repository.model.api.RepositoryNotFoundException; import org.lilyproject.util.zookeeper.ZkUtil; import org.lilyproject.util.zookeeper.ZooKeeperItf; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import static org.apache.zookeeper.Watcher.Event.EventType.NodeChildrenChanged; import static org.apache.zookeeper.Watcher.Event.EventType.NodeDataChanged; import static org.lilyproject.repository.model.api.RepositoryDefinition.RepositoryLifecycleState; public class RepositoryModelImpl implements RepositoryModel { private Map<String, RepositoryDefinition> repos = new HashMap<String, RepositoryDefinition>(); /** * Lock to be obtained when changing repos or when dispatching events. This assures the correct functioning * of methods like {@link #getRepositories(org.lilyproject.repository.model.api.RepositoryModelListener)}. */ private final Object reposLock = new Object(); private ZooKeeperItf zk; private final Set<RepositoryModelListener> listeners = Collections.newSetFromMap(new IdentityHashMap<RepositoryModelListener, Boolean>()); private Watcher zkWatcher; private boolean closed = false; private static final String REPOSITORY_COLLECTION_PATH = "/lily/repositorymodel/repositories"; private static final String REPOSITORY_COLLECTION_PATH_SLASH = REPOSITORY_COLLECTION_PATH + "/"; private final Log log = LogFactory.getLog(getClass()); /** * The default repository always exists and is always in state active. */ private static final RepositoryDefinition DEFAULT_REPOSITORY = new RepositoryDefinition("default", RepositoryLifecycleState.ACTIVE); public RepositoryModelImpl(ZooKeeperItf zk) throws KeeperException, InterruptedException { this.zk = zk; init(); } public void init() throws KeeperException, InterruptedException { ZkUtil.createPath(zk, REPOSITORY_COLLECTION_PATH); assureDefaultRepositoryExists(); zkWatcher = new RepositoryZkWatcher(); zk.addDefaultWatcher(zkWatcher); refresh(); } private void assureDefaultRepositoryExists() throws KeeperException, InterruptedException { byte[] repoBytes = RepositoryDefinitionJsonSerDeser.INSTANCE.toJsonBytes(DEFAULT_REPOSITORY); try { zk.create(REPOSITORY_COLLECTION_PATH + "/" + DEFAULT_REPOSITORY.getName(), repoBytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } catch (KeeperException.NodeExistsException e) { // it already exists, fine } } public void close() { zk.removeDefaultWatcher(zkWatcher); closed = true; } @Override public void create(String repositoryName) throws RepositoryExistsException, InterruptedException, RepositoryModelException { if (!RepoDefUtil.isValidRepositoryName(repositoryName)) { throw new IllegalArgumentException(String.format("'%s' is not a valid repository name. " + RepoDefUtil.VALID_NAME_EXPLANATION, repositoryName)); } RepositoryDefinition repoDef = new RepositoryDefinition(repositoryName, RepositoryLifecycleState.CREATE_REQUESTED); byte[] repoBytes = RepositoryDefinitionJsonSerDeser.INSTANCE.toJsonBytes(repoDef); try { zk.create(REPOSITORY_COLLECTION_PATH + "/" + repositoryName, repoBytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } catch (KeeperException.NodeExistsException e) { throw new RepositoryExistsException("Can't create repository, a repository with this name already exists: " + repositoryName); } catch (KeeperException e) { throw new RepositoryModelException(e); } } private void disallowDefaultRepository(String repositoryName) throws RepositoryModelException { if (DEFAULT_REPOSITORY.getName().equals(repositoryName)) { throw new RepositoryModelException("This operation is not allowed on the default repository."); } } @Override public void delete(String repositoryName) throws InterruptedException, RepositoryModelException, RepositoryNotFoundException { disallowDefaultRepository(repositoryName); RepositoryDefinition repoDef = new RepositoryDefinition(repositoryName, RepositoryLifecycleState.DELETE_REQUESTED); try { storeRepository(repoDef); } catch (KeeperException.NoNodeException e) { throw new RepositoryNotFoundException("Can't delete-request repository, a repository with this name doesn't exist: " + repoDef.getName()); } catch (KeeperException e) { throw new RepositoryModelException(e); } } @Override public void deleteDirect(String repositoryName) throws InterruptedException, RepositoryModelException, RepositoryNotFoundException { disallowDefaultRepository(repositoryName); try { zk.delete(REPOSITORY_COLLECTION_PATH + "/" + repositoryName, -1); } catch (KeeperException.NoNodeException e) { throw new RepositoryNotFoundException("Can't delete repository, a repository with this name doesn't exist: " + repositoryName); } catch (KeeperException e) { throw new RepositoryModelException("Error deleting repository.", e); } } @Override public void updateRepository(RepositoryDefinition repoDef) throws InterruptedException, RepositoryModelException,RepositoryNotFoundException { disallowDefaultRepository(repoDef.getName()); try { storeRepository(repoDef); } catch (KeeperException.NoNodeException e) { throw new RepositoryNotFoundException("Can't update repository, a repository with this name doesn't exist: " + repoDef.getName()); } catch (KeeperException e) { throw new RepositoryModelException(e); } } public void storeRepository(RepositoryDefinition repoDef) throws InterruptedException, KeeperException { byte[] repoBytes = RepositoryDefinitionJsonSerDeser.INSTANCE.toJsonBytes(repoDef); zk.setData(REPOSITORY_COLLECTION_PATH + "/" + repoDef.getName(), repoBytes, -1); } @Override public Set<RepositoryDefinition> getRepositories() throws RepositoryModelException, InterruptedException { try { return new HashSet<RepositoryDefinition>(loadRepositories(false).values()); } catch (KeeperException e) { throw new RepositoryModelException("Error loading repositories.", e); } } @Override public RepositoryDefinition getRepository(String repositoryName) throws InterruptedException, RepositoryModelException, RepositoryNotFoundException { try { return loadRepository(repositoryName, false); } catch (KeeperException.NoNodeException e) { throw new RepositoryNotFoundException("No repository named " + repositoryName, e); } catch (KeeperException e) { throw new RepositoryModelException("Error loading repository " + repositoryName, e); } } @Override public boolean repositoryExistsAndActive(String repositoryName) { RepositoryDefinition repoDef = repos.get(repositoryName); return repoDef != null && repoDef.getLifecycleState() == RepositoryLifecycleState.ACTIVE; } @Override public boolean repositoryActive(String repositoryName) throws RepositoryNotFoundException { RepositoryDefinition repoDef = repos.get(repositoryName); if (repoDef == null) { throw new RepositoryNotFoundException("No repository named " + repositoryName); } return repoDef.getLifecycleState() == RepositoryLifecycleState.ACTIVE; } @Override public boolean waitUntilRepositoryInState(String repositoryName, RepositoryLifecycleState state, long timeout) throws InterruptedException { long waitUntil = System.currentTimeMillis() + timeout; while (!repositoryExistsAndActive(repositoryName) && System.currentTimeMillis() < waitUntil) { Thread.sleep(50); } return repositoryExistsAndActive(repositoryName); } private class RepositoryZkWatcher implements Watcher { @Override public void process(WatchedEvent event) { boolean needsRefresh = false; if (event.getType() == Event.EventType.None && event.getState() == Event.KeeperState.SyncConnected) { needsRefresh = true; } else if (NodeChildrenChanged.equals(event.getType()) && event.getPath().equals(REPOSITORY_COLLECTION_PATH)) { needsRefresh = true; } else if (NodeDataChanged.equals(event.getType()) && event.getPath().startsWith(REPOSITORY_COLLECTION_PATH_SLASH)) { needsRefresh = true; } if (needsRefresh) { try { refresh(); } catch (Throwable t) { log.error("Repository Model: error handling event from ZooKeeper. Event: " + event, t); } } } } private void refresh() throws KeeperException, InterruptedException { List<RepositoryModelEvent> events = new ArrayList<RepositoryModelEvent>(); synchronized (reposLock) { Map<String, RepositoryDefinition> newRepos = loadRepositories(true); // Find out changes in repositories Set<String> removedRepos = Sets.difference(repos.keySet(), newRepos.keySet()); for (String id : removedRepos) { events.add(new RepositoryModelEvent(RepositoryModelEventType.REPOSITORY_REMOVED, id)); } Set<String> addedRepos = Sets.difference(newRepos.keySet(), repos.keySet()); for (String id : addedRepos) { events.add(new RepositoryModelEvent(RepositoryModelEventType.REPOSITORY_ADDED, id)); } for (RepositoryDefinition repoDef : newRepos.values()) { if (repos.containsKey(repoDef.getName()) && !repos.get(repoDef.getName()).equals(repoDef)) { events.add(new RepositoryModelEvent(RepositoryModelEventType.REPOSITORY_UPDATED, repoDef.getName())); } } repos = newRepos; } notifyListeners(events); } private Map<String, RepositoryDefinition> loadRepositories(boolean watch) throws KeeperException, InterruptedException { Map<String, RepositoryDefinition> repositories = new HashMap<String, RepositoryDefinition>(); List<String> children = zk.getChildren(REPOSITORY_COLLECTION_PATH, watch ? zkWatcher : null); for (String child : children) { repositories.put(child, loadRepository(child, watch)); } return repositories; } private RepositoryDefinition loadRepository(String name, boolean watch) throws KeeperException, InterruptedException { byte[] repoJson = zk.getData(REPOSITORY_COLLECTION_PATH + "/" + name, watch ? zkWatcher : null, new Stat()); return RepositoryDefinitionJsonSerDeser.INSTANCE.fromJsonBytes(name, repoJson); } private void notifyListeners(List<RepositoryModelEvent> events) { if (closed) { // Stop dispatching events once closed return; } for (RepositoryModelEvent event : events) { for (RepositoryModelListener listener : listeners) { listener.process(event); } } } @Override public Set<RepositoryDefinition> getRepositories(RepositoryModelListener listener) { synchronized (reposLock) { registerListener(listener); return new HashSet<RepositoryDefinition>(repos.values()); } } @Override public void registerListener(RepositoryModelListener listener) { synchronized (reposLock) { listeners.add(listener); } } @Override public void unregisterListener(RepositoryModelListener listener) { listeners.remove(listener); } }