/* * Copyright 2013-2015 EMC Corporation. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://www.apache.org/licenses/LICENSE-2.0.txt * * or in the "license" file accompanying this file. This file 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 com.emc.ecs.sync.service; import com.emc.ecs.sync.EcsSync; import com.emc.ecs.sync.SyncStats; import com.emc.ecs.sync.config.SyncConfig; import com.emc.ecs.sync.config.SyncOptions; import com.emc.ecs.sync.rest.*; import com.emc.ecs.sync.util.SyncUtil; import com.sun.management.OperatingSystemMXBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.lang.management.ManagementFactory; import java.util.Collections; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; public class SyncJobService { private static final Logger log = LoggerFactory.getLogger(SyncJobService.class); public static final LogLevel DEFAULT_LOG_LEVEL = LogLevel.quiet; public static final int MAX_JOBS = 10; // maximum of 10 sync jobs per JVM process private static SyncJobService instance; public static synchronized SyncJobService getInstance() { if (instance == null) { instance = new SyncJobService(); } return instance; } private String dbConnectString; private Map<Integer, EcsSync> syncCache = new TreeMap<>(); private Map<Integer, SyncConfig> configCache = new TreeMap<>(); private AtomicInteger nextJobId = new AtomicInteger(0); private LogLevel logLevel = DEFAULT_LOG_LEVEL; public LogLevel getLogLevel() { return logLevel; } // Note: now that we use slf4j, this will *only* take effect if the log implementation is log4j public void setLogLevel(LogLevel logLevel) { // try to avoid a runtime dependency on log4j (untested) try { org.apache.log4j.Logger rootLogger = org.apache.log4j.LogManager.getRootLogger(); if (LogLevel.debug == logLevel) { rootLogger.setLevel(org.apache.log4j.Level.DEBUG); java.util.logging.LogManager.getLogManager().getLogger("").setLevel(Level.FINE); } else if (LogLevel.verbose == logLevel) { rootLogger.setLevel(org.apache.log4j.Level.INFO); java.util.logging.LogManager.getLogManager().getLogger("").setLevel(Level.INFO); } else if (LogLevel.quiet == logLevel) { rootLogger.setLevel(org.apache.log4j.Level.WARN); java.util.logging.LogManager.getLogManager().getLogger("").setLevel(Level.WARNING); } else if (LogLevel.silent == logLevel) { rootLogger.setLevel(org.apache.log4j.Level.ERROR); java.util.logging.LogManager.getLogManager().getLogger("").setLevel(Level.SEVERE); } org.apache.log4j.AppenderSkeleton mainAppender = (org.apache.log4j.AppenderSkeleton) rootLogger.getAppender("mainAppender"); org.apache.log4j.AppenderSkeleton stackAppender = (org.apache.log4j.AppenderSkeleton) rootLogger.getAppender("stacktraceAppender"); if (logLevel.isIncludeStackTrace()) { if (mainAppender != null) mainAppender.setThreshold(org.apache.log4j.Level.OFF); if (stackAppender != null) stackAppender.setThreshold(org.apache.log4j.Level.ALL); } else { if (mainAppender != null) mainAppender.setThreshold(org.apache.log4j.Level.ALL); if (stackAppender != null) stackAppender.setThreshold(org.apache.log4j.Level.OFF); } this.logLevel = logLevel; } catch (Throwable t) { log.warn("could not configure log4j (perhaps you're using a different logger, which is fine)", t); } } public JobList getAllJobs() { JobList jobList = new JobList(); for (Map.Entry<Integer, EcsSync> entry : syncCache.entrySet()) { jobList.getJobs().add(new JobInfo(entry.getKey(), getJobStatus(entry.getValue()), configCache.get(entry.getKey()), getProgress(entry.getKey()))); } return jobList; } public boolean jobExists(int jobId) { return syncCache.containsKey(jobId); } public int createJob(SyncConfig syncConfig) { if (syncCache.size() >= MAX_JOBS) throw new UnsupportedOperationException("the maximum number of jobs (" + MAX_JOBS + ") has been reached"); int jobId = nextJobId.incrementAndGet(); // set connect string only if table is specified or EcsSync will create a db service with the default table SyncOptions options = syncConfig.getOptions(); if (options.getDbTable() != null && options.getDbConnectString() == null && dbConnectString != null) options.setDbConnectString(dbConnectString); EcsSync sync = new EcsSync(); sync.setSyncConfig(syncConfig); syncCache.put(jobId, sync); configCache.put(jobId, syncConfig); // start a background thread (otherwise this will block until the entire sync is done!) ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(new SyncTask(jobId, sync)); executor.shutdown(); return jobId; } public int registerJob(EcsSync sync) { int jobId = nextJobId.incrementAndGet(); syncCache.put(jobId, sync); return jobId; } public SyncConfig getJob(int jobId) { if (syncCache.containsKey(jobId)) { if (!configCache.containsKey(jobId)) throw new UnsupportedOperationException("specified job was created externally"); return configCache.get(jobId); } return null; } public void deleteJob(int jobId, boolean keepDatabase) { EcsSync sync = syncCache.get(jobId); if (sync == null) throw new IllegalArgumentException("the specified job ID does not exist"); if (!getJobStatus(sync).isFinalState()) throw new UnsupportedOperationException("the job must be stopped before it can be deleted"); syncCache.remove(jobId); configCache.remove(jobId); // delete database if (sync.getDbService() != null) { if (!keepDatabase) sync.getDbService().deleteDatabase(); try { sync.getDbService().close(); } catch (IOException e) { log.warn("could not close database", e); } } } public JobControl getJobControl(int jobId) { EcsSync sync = syncCache.get(jobId); if (sync == null) return null; JobControl jobControl = new JobControl(); jobControl.setStatus(getJobStatus(sync)); jobControl.setThreadCount(sync.getSyncConfig().getOptions().getThreadCount()); return jobControl; } public void setJobControl(int jobId, JobControl jobControl) { EcsSync sync = syncCache.get(jobId); if (sync == null) throw new IllegalArgumentException("the specified job ID does not exist"); if (jobControl.getThreadCount() > 0) { sync.setThreadCount(jobControl.getThreadCount()); } if (jobControl.getStatus() != null) { switch (jobControl.getStatus()) { case Stopped: sync.terminate(); break; case Paused: sync.pause(); break; case Running: sync.resume(); break; } } } public SyncProgress getProgress(int jobId) { EcsSync sync = syncCache.get(jobId); if (sync == null) return null; SyncStats stats = sync.getStats(); SyncProgress syncProgress = new SyncProgress(); syncProgress.setStatus(getJobStatus(sync)); syncProgress.setSyncStartTime(stats.getStartTime()); syncProgress.setSyncStopTime(stats.getStopTime()); syncProgress.setEstimatingTotals(sync.isEstimating()); syncProgress.setTotalBytesExpected(sync.getEstimatedTotalBytes()); syncProgress.setTotalObjectsExpected(sync.getEstimatedTotalObjects()); syncProgress.setBytesComplete(stats.getBytesComplete()); syncProgress.setBytesSkipped(stats.getBytesSkipped()); syncProgress.setObjectsComplete(stats.getObjectsComplete()); syncProgress.setObjectsSkipped(stats.getObjectsSkipped()); syncProgress.setObjectsFailed(stats.getObjectsFailed()); syncProgress.setObjectsAwaitingRetry(sync.getObjectsAwaitingRetry()); syncProgress.setActiveQueryTasks(sync.getActiveQueryThreads()); syncProgress.setActiveSyncTasks(sync.getActiveSyncThreads()); syncProgress.setRuntimeMs(stats.getTotalRunTime()); syncProgress.setCpuTimeMs(stats.getTotalCpuTime()); OperatingSystemMXBean osBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); syncProgress.setProcessCpuLoad(osBean.getProcessCpuLoad()); syncProgress.setProcessMemoryUsed(Runtime.getRuntime().totalMemory()); // it's possible that the sync instance has not initialized its plugins yet if (sync.getSource() != null) { syncProgress.setSourceReadRate(sync.getSource().getReadRate()); syncProgress.setSourceWriteRate(sync.getSource().getWriteRate()); } if (sync.getTarget() != null) { syncProgress.setTargetReadRate(sync.getTarget().getReadRate()); syncProgress.setTargetWriteRate(sync.getTarget().getWriteRate()); } syncProgress.setObjectCompleteRate(sync.getStats().getObjectCompleteRate()); syncProgress.setObjectSkipRate(sync.getStats().getObjectSkipRate()); syncProgress.setObjectErrorRate(sync.getStats().getObjectErrorRate()); if (sync.getRunError() != null) syncProgress.setRunError(SyncUtil.summarize(sync.getRunError())); return syncProgress; } public Iterable<SyncRecord> getAllRecords(int jobId) { EcsSync sync = syncCache.get(jobId); if (sync == null) return null; if (sync.getDbService() == null) return Collections.emptyList(); else return sync.getDbService().getAllRecords(); } public Iterable<SyncRecord> getSyncErrors(int jobId) { EcsSync sync = syncCache.get(jobId); if (sync == null) return null; if (sync.getDbService() == null) return Collections.emptyList(); else return sync.getDbService().getSyncErrors(); } protected JobControlStatus getJobStatus(EcsSync sync) { if (sync.isPaused()) return JobControlStatus.Paused; if (sync.isRunning()) return JobControlStatus.Running; if (sync.isTerminated()) { if (sync.getActiveSyncThreads() > 0) return JobControlStatus.Stopping; else return JobControlStatus.Stopped; } if (sync.getStats().getStopTime() > 0) return JobControlStatus.Complete; return JobControlStatus.Initializing; } public String getDbConnectString() { return dbConnectString; } /** * Sets the JDBC connect string for the mySQL database that will be used by all syncs specifying a dbTable */ public void setDbConnectString(String dbConnectString) { this.dbConnectString = dbConnectString; } protected class SyncTask implements Runnable { private int jobId; private EcsSync sync; public SyncTask(int jobId, EcsSync sync) { this.jobId = jobId; this.sync = sync; } @Override public void run() { try { sync.run(); } catch (Throwable t) { log.error("sync job " + jobId + " threw an unexpected error", t); } } } }