package co.codewizards.cloudstore.core.repo.sync; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.Severity; import co.codewizards.cloudstore.core.config.Config; import co.codewizards.cloudstore.core.config.ConfigImpl; import co.codewizards.cloudstore.core.dto.Error; import co.codewizards.cloudstore.core.oio.File; import co.codewizards.cloudstore.core.repo.local.LocalRepoHelper; import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory; public class RepoSyncDaemonImpl implements RepoSyncDaemon { private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); private Set<RepoSyncQueueItem> syncQueue = new LinkedHashSet<>(); private Map<UUID, RepoSyncRunner> repositoryId2SyncRunner = new HashMap<>(); private final ExecutorService executorService; private Map<UUID, Set<RepoSyncActivity>> repositoryId2SyncActivities = new HashMap<>(); private Map<UUID, List<RepoSyncState>> repositoryId2SyncStates = new HashMap<>(); private static final AtomicInteger threadGroupIndex = new AtomicInteger(); private final AtomicInteger threadIndex = new AtomicInteger(); private static final class Holder { public static final RepoSyncDaemonImpl instance = new RepoSyncDaemonImpl(); } protected RepoSyncDaemonImpl() { final int tgi = threadGroupIndex.getAndIncrement(); final ThreadGroup threadGroup = new ThreadGroup("RepoSyncDaemonThreadGroup_" + tgi); executorService = Executors.newCachedThreadPool(new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(threadGroup, r, "RepoSyncDaemonThread_" + tgi + "_" + threadIndex.getAndIncrement()); } }); } public static RepoSyncDaemon getInstance() { return Holder.instance; } @Override public UUID startSync(final File file) { assertNotNull(file, "file"); final File localRoot = LocalRepoHelper.getLocalRootContainingFile(file); if (localRoot == null) throw new IllegalArgumentException("File is not located inside a local repository: " + file); final UUID repositoryId; try (final LocalRepoManager localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRoot);) { repositoryId = localRepoManager.getRepositoryId(); } final RepoSyncQueueItem repoSyncQueueItem = new RepoSyncQueueItem(repositoryId, localRoot); enqueue(repoSyncQueueItem); startSyncWithNextSyncQueueItem(repositoryId); updateActivities(repositoryId); return repositoryId; } private synchronized void enqueue(final RepoSyncQueueItem repoSyncQueueItem) { syncQueue.add(repoSyncQueueItem); } private synchronized void startSyncWithNextSyncQueueItem(final UUID repositoryId) { assertNotNull(repositoryId, "repositoryId"); if (!repositoryId2SyncRunner.containsKey(repositoryId)) { final RepoSyncQueueItem nextSyncQueueItem = pollSyncQueueItem(repositoryId); if (nextSyncQueueItem != null) { final RepoSyncRunner repoSyncRunner = new RepoSyncRunner(nextSyncQueueItem); repositoryId2SyncRunner.put(nextSyncQueueItem.repositoryId, repoSyncRunner); submitToExecutorService(nextSyncQueueItem); } } } private void submitToExecutorService(final RepoSyncQueueItem repoSyncQueueItem) { final RepoSyncRunner repoSyncRunner = new RepoSyncRunner(repoSyncQueueItem); synchronized (this) { repositoryId2SyncRunner.put(repoSyncQueueItem.repositoryId, repoSyncRunner); } executorService.submit(new WrapperRunnable(repoSyncRunner)); } private class WrapperRunnable implements Runnable { private final Logger logger = LoggerFactory.getLogger(RepoSyncDaemonImpl.WrapperRunnable.class); private final UUID repositoryId; private final RepoSyncRunner repoSyncRunner; public WrapperRunnable(final RepoSyncRunner repoSyncRunner) { this.repoSyncRunner = assertNotNull(repoSyncRunner, "repoSyncRunner"); this.repositoryId = repoSyncRunner.getSyncQueueItem().repositoryId; } @Override public void run() { try { repoSyncRunner.run(); registerSyncSuccess(repoSyncRunner); } catch (final Throwable x) { logger.error("run: " + x, x); registerSyncError(repoSyncRunner, x); } synchronized (RepoSyncDaemonImpl.this) { final RepoSyncRunner removed = repositoryId2SyncRunner.remove(repositoryId); if (removed != repoSyncRunner) logger.error("run: removed != repoSyncRunner"); startSyncWithNextSyncQueueItem(repositoryId); } updateActivities(repositoryId); } } private void registerSyncSuccess(final RepoSyncRunner repoSyncRunner) { assertNotNull(repoSyncRunner, "repoSyncRunner"); final List<RepoSyncState> statesAdded = new ArrayList<RepoSyncState>(); final List<RepoSyncState> statesRemoved; final UUID localRepositoryId = repoSyncRunner.getSyncQueueItem().repositoryId; final File localRoot = repoSyncRunner.getSyncQueueItem().localRoot; synchronized (this) { final List<RepoSyncState> list = _getRepoSyncStates(localRepositoryId); for (final Map.Entry<UUID, URL> me : repoSyncRunner.getRemoteRepositoryId2RemoteRootMap().entrySet()) { final UUID remoteRepositoryId = me.getKey(); final URL remoteRoot = me.getValue(); final RepoSyncState state = new RepoSyncState(localRepositoryId, remoteRepositoryId, localRoot, remoteRoot, Severity.INFO, "Sync OK.", null, repoSyncRunner.getSyncStarted(), repoSyncRunner.getSyncFinished()); list.add(state); statesAdded.add(state); } statesRemoved = evictOldStates(localRepositoryId, localRoot); } firePropertyChange(PropertyEnum.states_added, null, Collections.unmodifiableList(statesAdded)); if (! statesRemoved.isEmpty()) firePropertyChange(PropertyEnum.states_removed, null, Collections.unmodifiableList(statesRemoved)); firePropertyChange(PropertyEnum.states, null, getStates(localRepositoryId)); } private void registerSyncError(final RepoSyncRunner repoSyncRunner, final Throwable exception) { assertNotNull(repoSyncRunner, "repoSyncRunner"); assertNotNull(exception, "exception"); final List<RepoSyncState> statesAdded = new ArrayList<RepoSyncState>(); final List<RepoSyncState> statesRemoved; final UUID localRepositoryId = repoSyncRunner.getSyncQueueItem().repositoryId; final File localRoot = repoSyncRunner.getSyncQueueItem().localRoot; synchronized (this) { final List<RepoSyncState> list = _getRepoSyncStates(localRepositoryId); UUID remoteRepositoryId = repoSyncRunner.getRemoteRepositoryId(); URL remoteRoot = repoSyncRunner.getRemoteRoot(); final RepoSyncState state = new RepoSyncState(localRepositoryId, remoteRepositoryId, localRoot, remoteRoot, Severity.ERROR, exception.getMessage(), new Error(exception), repoSyncRunner.getSyncStarted(), repoSyncRunner.getSyncFinished()); if (remoteRepositoryId != null && remoteRoot != null) { list.add(state); statesAdded.add(state); } else { for (Map.Entry<UUID, URL> me : repoSyncRunner.getRemoteRepositoryId2RemoteRootMap().entrySet()) { remoteRepositoryId = me.getKey(); remoteRoot = me.getValue(); list.add(state); statesAdded.add(state); } } statesRemoved = evictOldStates(localRepositoryId, localRoot); } firePropertyChange(PropertyEnum.states_added, null, Collections.unmodifiableList(statesAdded)); if (! statesRemoved.isEmpty()) firePropertyChange(PropertyEnum.states_removed, null, Collections.unmodifiableList(statesRemoved)); firePropertyChange(PropertyEnum.states, null, getStates(localRepositoryId)); } private List<RepoSyncState> _getRepoSyncStates(final UUID localRepositoryId) { List<RepoSyncState> list = repositoryId2SyncStates.get(localRepositoryId); if (list == null) { list = new LinkedList<>(); repositoryId2SyncStates.put(localRepositoryId, list); } return list; } private synchronized List<RepoSyncState> evictOldStates(final UUID localRepositoryId, final File localRoot) { assertNotNull(localRepositoryId, "localRepositoryId"); assertNotNull(localRoot, "localRoot"); final List<RepoSyncState> evicted = new ArrayList<RepoSyncState>(); final Config config = ConfigImpl.getInstanceForDirectory(localRoot); final int syncStatesMaxSize = config.getPropertyAsPositiveOrZeroInt(CONFIG_KEY_SYNC_STATES_MAX_SIZE, DEFAULT_SYNC_STATES_MAX_SIZE); final List<RepoSyncState> list = repositoryId2SyncStates.get(localRepositoryId); if (list != null) { // Note: This implementation is not very efficient, but the list usually has a size of only a few // entries - rarely ever more than a few dozen. Thus, this algorithm is certainly fast enough ;-) for (final Iterator<RepoSyncState> it = list.iterator(); it.hasNext();) { final RepoSyncState repoSyncState = it.next(); if (getSyncStatesSizeForServerRepositoryId(list, repoSyncState.getServerRepositoryId()) > syncStatesMaxSize) { evicted.add(repoSyncState); it.remove(); } } } return evicted; } private int getSyncStatesSizeForServerRepositoryId(final List<RepoSyncState> repoSyncStates, final UUID serverRepositoryId) { assertNotNull(serverRepositoryId, "serverRepositoryId"); int result = 0; for (RepoSyncState repoSyncState : repoSyncStates) { if (serverRepositoryId.equals(repoSyncState.getServerRepositoryId())) ++result; } return result; } private synchronized RepoSyncQueueItem pollSyncQueueItem(UUID repositoryId) { assertNotNull(repositoryId, "repositoryId"); for (Iterator<RepoSyncQueueItem> it = syncQueue.iterator(); it.hasNext(); ) { final RepoSyncQueueItem repoSyncQueueItem = it.next(); if (repositoryId.equals(repoSyncQueueItem.repositoryId)) { it.remove(); return repoSyncQueueItem; } } return null; } @Override public synchronized List<RepoSyncState> getStates(final UUID localRepositoryId) { assertNotNull(localRepositoryId, "localRepositoryId"); final List<RepoSyncState> list = repositoryId2SyncStates.get(localRepositoryId); if (list == null) return Collections.emptyList(); else return Collections.unmodifiableList(new ArrayList<>(list)); } @Override public synchronized Set<RepoSyncActivity> getActivities(final UUID localRepositoryId) { assertNotNull(localRepositoryId, "localRepositoryId"); final Set<RepoSyncActivity> activities = repositoryId2SyncActivities.get(localRepositoryId); if (activities == null) return Collections.emptySet(); return Collections.unmodifiableSet(new HashSet<RepoSyncActivity>(activities)); } private void updateActivities(final UUID localRepositoryId) { assertNotNull(localRepositoryId, "localRepositoryId"); final List<RepoSyncActivity> activitiesAdded = new ArrayList<RepoSyncActivity>(); final List<RepoSyncActivity> activitiesRemoved = new ArrayList<RepoSyncActivity>(); synchronized (this) { Set<RepoSyncActivity> activities = repositoryId2SyncActivities.get(localRepositoryId); if (activities == null) { activities = new HashSet<RepoSyncActivity>(2); repositoryId2SyncActivities.put(localRepositoryId, activities); } final RepoSyncRunner repoSyncRunner = repositoryId2SyncRunner.get(localRepositoryId); if (repoSyncRunner == null) { final List<RepoSyncActivity> activitiesStale = _findActivities(localRepositoryId, RepoSyncActivityType.IN_PROGRESS); activitiesRemoved.addAll(activitiesStale); activities.removeAll(activitiesStale); } else { final RepoSyncActivity activity = new RepoSyncActivity( repoSyncRunner.getSyncQueueItem().repositoryId, repoSyncRunner.getSyncQueueItem().localRoot, RepoSyncActivityType.IN_PROGRESS); if (activities.add(activity)) activitiesAdded.add(activity); } final List<RepoSyncQueueItem> queueItems = _findQueueItems(localRepositoryId); if (queueItems.isEmpty()) { final List<RepoSyncActivity> activitiesStale = _findActivities(localRepositoryId, RepoSyncActivityType.QUEUED); activitiesRemoved.addAll(activitiesStale); activities.removeAll(activitiesStale); } else { final RepoSyncActivity activity = new RepoSyncActivity( repoSyncRunner.getSyncQueueItem().repositoryId, repoSyncRunner.getSyncQueueItem().localRoot, RepoSyncActivityType.QUEUED); if (activities.add(activity)) activitiesAdded.add(activity); } } if (! activitiesRemoved.isEmpty()) firePropertyChange(PropertyEnum.activities_removed, null, activitiesRemoved); if (! activitiesAdded.isEmpty()) firePropertyChange(PropertyEnum.activities_added, null, activitiesAdded); if (! activitiesAdded.isEmpty() || ! activitiesRemoved.isEmpty()) firePropertyChange(PropertyEnum.activities, null, getActivities(localRepositoryId)); } private synchronized List<RepoSyncActivity> _findActivities(final UUID localRepositoryId, final RepoSyncActivityType activityType) { assertNotNull(localRepositoryId, "localRepositoryId"); final Set<RepoSyncActivity> activities = repositoryId2SyncActivities.get(localRepositoryId); if (activities == null) return Collections.emptyList(); final List<RepoSyncActivity> result = new ArrayList<>(1); for (RepoSyncActivity activity : activities) { if (activity.getActivityType() == activityType) result.add(activity); } return Collections.unmodifiableList(result); } private synchronized List<RepoSyncQueueItem> _findQueueItems(final UUID localRepositoryId) { assertNotNull(localRepositoryId, "localRepositoryId"); final List<RepoSyncQueueItem> result = new ArrayList<RepoSyncQueueItem>(2); for (final RepoSyncQueueItem queueItem : syncQueue) { if (localRepositoryId.equals(queueItem.repositoryId)) result.add(queueItem); } return Collections.unmodifiableList(result); } @Override public void shutdown() { executorService.shutdown(); } @Override public void shutdownNow() { executorService.shutdownNow(); } @Override public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } @Override public void addPropertyChangeListener(Property property, PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(property.name(), listener); } @Override public void removePropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); } @Override public void removePropertyChangeListener(Property property, PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(property.name(), listener); } protected void firePropertyChange(Property property, Object oldValue, Object newValue) { propertyChangeSupport.firePropertyChange(property.name(), oldValue, newValue); } }