All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.structr.cloud.sync.SyncService Maven / Gradle / Ivy

Go to download

Structr is an open source framework based on the popular Neo4j graph database.

The newest version!
/**
 * Copyright (C) 2010-2016 Structr GmbH
 *
 * This file is part of Structr .
 *
 * Structr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Structr is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Structr.  If not, see .
 */
package org.structr.cloud.sync;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang.StringUtils;
import org.structr.cloud.CloudHost;
import org.structr.cloud.CloudListener;
import org.structr.cloud.CloudService;
import org.structr.cloud.transmission.SingleTransmission;
import org.structr.common.SecurityContext;
import org.structr.common.error.FrameworkException;
import org.structr.api.service.Command;
import org.structr.api.service.RunnableService;
import org.structr.api.service.StructrServices;
import org.structr.core.StructrTransactionListener;
import org.structr.core.TransactionSource;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.graph.ModificationEvent;
import org.structr.core.graph.TransactionCommand;
import org.structr.core.graph.Tx;

/**
 *
 *
 */
public class SyncService extends Thread  implements RunnableService, StructrTransactionListener {

	private static final BlockingQueue> syncQueue = new ArrayBlockingQueue<>(1000);
	private static final Logger logger                                    = Logger.getLogger(CloudService.class.getName());

	public enum SyncRole {
		master,
		slave
	};

	private final List syncHosts = new LinkedList<>();
	private boolean running                    = false;
	private boolean active                     = false;
	private String allowedMaster               = null;
	private SyncRole role                      = null;
	private int requiredSyncCount              = 0;
	private int retryInterval                  = 60;

	public SyncService() {

		super("SyncService");
		this.setDaemon(true);
	}

	@Override
	public void injectArguments(Command command) {
	}

	@Override
	public void initialize(final StructrServices services, final Properties config) {

		active = "true".equals(config.getProperty("sync.enabled", "false"));

		if (active) {

			// initialize role and master
			role          = SyncRole.valueOf(config.getProperty("sync.role", "master"));
			allowedMaster = config.getProperty("sync.master", null);

			if (allowedMaster == null && SyncRole.slave.equals(role)) {
				throw new IllegalStateException("no master address set for this slave, please set sync.master in structr.conf.");
			}

			final String minimum = config.getProperty("sync.minimum", "1");
			final String retry   = config.getProperty("sync.retry", "60");
			final String hosts   = config.getProperty("sync.hosts");
			final String users   = config.getProperty("sync.users");
			final String pwds    = config.getProperty("sync.passwords");
			final String ports   = config.getProperty("sync.ports");

			// check only if we are a replication master
			if (SyncRole.master.equals(role)) {

				if (StringUtils.isEmpty(hosts)) {
					throw new IllegalStateException("no slave hosts set for this master, please set sync.hosts in structr.conf.");
				}

				if (StringUtils.isEmpty(users)) {
					throw new IllegalStateException("no slave users set for this master, please set sync.users in structr.conf.");
				}

				if (StringUtils.isEmpty(pwds)) {
					throw new IllegalStateException("no slave passwords set for this master, please set sync.passwords in structr.conf.");
				}

				if (StringUtils.isEmpty(ports)) {
					throw new IllegalStateException("no slave ports set for this master, please set sync.ports in structr.conf.");
				}

				final String[] remoteHosts = hosts != null ? hosts.split("[, ]+") : new String[0];
				final String[] remoteUsers = users != null ? users.split("[, ]+") : new String[0];
				final String[] remotePwds  = pwds  != null ? pwds.split("[, ]+")  : new String[0];
				final String[] remotePorts = ports != null ? ports.split("[, ]+") : new String[0];

				String previousUser = null;
				String previousPwd  = null;
				String previousPort = null;

				for (int i=0; i i ? remoteUsers[i] : previousUser;
					final String pwd  = remotePwds.length > i ?  remotePwds[i]   : previousPwd;
					final String port = remotePorts.length > i ? remotePorts[i] : previousPort;

					previousUser = user;
					previousPwd  = pwd;
					previousPort = port;

					if (StringUtils.isEmpty(user)) {
						throw new IllegalStateException("no sync user found for remote host " + host + ", please set sync.users in structr.conf.");
					}

					if (StringUtils.isEmpty(pwd)) {
						throw new IllegalStateException("no sync password found for remote host " + host + ", please set sync.passwords in structr.conf.");
					}

					if (StringUtils.isEmpty(port)) {
						throw new IllegalStateException("no sync port found for remote host " + host + ", please set sync.ports in structr.conf.");
					}

					final SyncHostInfo syncHostInfo = new SyncHostInfo(host, user, pwd, port);
					syncHosts.add(syncHostInfo);

					logger.log(Level.INFO, "Adding slave host {0}, user {2}", new Object[] { syncHostInfo, port, user } );
				}

				try {
					// check and initialize sync hosts and policy
					initializeSyncHosts(minimum);

				} catch (FrameworkException fex) {
					logger.log(Level.WARNING, "", fex);
				}
			}

			if (StringUtils.isNotBlank(retry)) {
				this.retryInterval = Integer.valueOf(retry);
			}

			logger.log(Level.INFO, "Retry interval is set to {0} seconds", retryInterval);
		}
	}

	@Override
	public void initialized() {}

	@Override
	public void shutdown() {
		running = false;
	}

	@Override
	public boolean isRunning() {
		return running;
	}

	@Override
	public void startService() {

		TransactionCommand.registerTransactionListener(this);

		running = true;
		start();

		logger.log(Level.INFO, "SyncService successfully started.");
	}

	@Override
	public void run() {

		while (running) {

			try {

				// wait to be notified when new data is available
				synchronized (syncQueue) {

					while (syncQueue.isEmpty()) {
						syncQueue.wait();
					}
				}

				// load the head of the queue without removing it
				// (will be removed later if sync was successful)
				final List transaction = syncQueue.peek();
				if (transaction != null) {

					// define success as "at least one sync process was successful"
					final SyncListener successListener = new SyncListener(requiredSyncCount);

					final SyncTransmission transmission = new SyncTransmission(transaction);

					for (final SyncHostInfo info : syncHosts) {

						try {

							CloudService.doRemote(SecurityContext.getSuperUserInstance(), transmission, info, successListener);

						} catch (FrameworkException fex) {
							logger.log(Level.WARNING, "Unable to synchronize with host {0}: {1}", new Object[] { info, fex.getMessage() } );
						}
					}

					// remove sync changeset from queue when sync
					// was successful (see above)
					if (successListener.wasSuccessful()) {

						syncQueue.remove(transaction);

					} else {

						logger.log(Level.WARNING, "Unable to synchronize with required number of hosts, retrying in {0} seconds..", retryInterval);

						// sleep
						try { Thread.sleep(retryInterval * 1000); } catch (Throwable t) {}
					}

				}

			} catch (Throwable t) {
				logger.log(Level.WARNING, "", t);
			}
		}
	}

	@Override
	public void stopService() {
		shutdown();
	}

	@Override
	public boolean runOnStartup() {
		return true;
	}


	@Override
	public boolean isVital() {
		return true;
	}

	// ----- interface StructrTransactionListener -----
	@Override
	public void beforeCommit(final SecurityContext securityContext, final Collection modificationEvents, final TransactionSource source) throws FrameworkException {

		// prevent all (!) transactions from being committed on slave instances
		if (SyncRole.slave.equals(role) && (source == null || !allowedMaster.equals(source.getOriginAddress()))) {

			if (modificationEvents != null && !modificationEvents.isEmpty()) {

				throw new FrameworkException(500, "Illegal write transaction on active slave.");
			}
		}
	}

	@Override
	public void afterCommit(final SecurityContext securityContext, final Collection modificationEvents, final TransactionSource source) {

		if (source != null && source.isRemote()) {
			return;
		}

		// only react if desired
		if (active && running && !modificationEvents.isEmpty()) {

			try {
				// store last sync timestamp for the given instance ID
				final App app = StructrApp.getInstance();
				app.setGlobalSetting(app.getInstanceId() + ".lastModified", System.currentTimeMillis());

			} catch (FrameworkException fex) {

				logger.log(Level.SEVERE, "Unable to store last modified date for current instance.", fex);
			}


			try {

				// copy all modification events and return quickly
				syncQueue.put(new ArrayList<>(modificationEvents));

				// notify sync queue of new input
				synchronized (syncQueue) { syncQueue.notify(); }

			} catch (InterruptedException iex) {
				logger.log(Level.WARNING, "", iex);
			}

		}
	}

	// ----- private methods -----
	private void initializeSyncHosts(final String minimum) throws FrameworkException {

		final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
		final String instanceId           = StructrApp.getInstance().getInstanceId();

		// check connection and version of sync host
		for (Iterator it = syncHosts.iterator(); it.hasNext();) {

			final SyncHostInfo host = it.next();
			boolean reachable       = true;

			try {

				final SingleTransmission transmission = new SingleTransmission<>(new ReplicationStatus(instanceId));
				final ReplicationStatus status = CloudService.doRemote(SecurityContext.getSuperUserInstance(), transmission, host, new LoggingListener());
				if (status != null) {

					final String slaveId = status.getSlaveId();
					if (slaveId != null ) {

						final long syncTimestamp  = status.getLastSync();
						String lastSyncString     = "not synced yet";

						if (syncTimestamp != 0L) {

							lastSyncString = "last sync was " + dateFormat.format(syncTimestamp);
						}

						logger.log(Level.INFO, "Determined instance ID of {0} to be {1}, {2}.", new Object[] { host, slaveId, lastSyncString } );

						// store replication status in host info
						host.setReplicationStatus(status);

					} else {

						reachable = false;
					}

				} else {

					reachable = false;
				}

			} catch (Throwable t) {

				logger.log(Level.WARNING, "", t);
				reachable = false;
			}

			if (!reachable) {

				logger.log(Level.WARNING, "Synchronization slave {0} not reachable, removing from list.", host);
				it.remove();
			}
		}

		// check number of synchronization hosts
		final int numSyncHosts  = syncHosts.size();
		requiredSyncCount       = Integer.valueOf(minimum);

		if (numSyncHosts < requiredSyncCount) {
			throw new IllegalStateException("synchronization policy requires at least " + requiredSyncCount + " hosts, but only " + numSyncHosts + " are reachable.");
		}

		logger.log(Level.INFO, "Synchronization to {0} host{1} required.", new Object[] { requiredSyncCount, requiredSyncCount == 1 ? "" : "s" } );


		// prepare synchronization hosts
		for (final SyncHostInfo host : syncHosts) {

			// try to copy database contents to synchronization slave
			checkAndInitializeSyncHost(host);
		}
	}

	private void checkAndInitializeSyncHost(final SyncHostInfo host) throws FrameworkException {

		final String masterId         = StructrApp.getInstance().getInstanceId();
		final SimpleDateFormat df     = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
		final long localSyncTimestamp = StructrApp.getInstance().getGlobalSetting(masterId + ".lastModified", 0L);

		if (localSyncTimestamp == 0L) {

			// no synchronization with this slave yet, clear and initialize slave database
			synchronizeSlave(host);

		} else {

			// there has been a synchronization in the past
			if (host.getLastSyncTimestamp() != localSyncTimestamp) {

				logger.log(Level.INFO, "Replication host {0} is out of sync, last remote update was {1} whereas last local update was {2}",
					new Object[] { host,  df.format(host.getLastSyncTimestamp()), df.format(localSyncTimestamp) }
				);

				// clear and re-initialize slave database..
				synchronizeSlave(host);

			} else {

				logger.log(Level.INFO, "Replication host {0} is in sync, last update was {1}", new Object[] { host, df.format(localSyncTimestamp) } );
			}
		}
	}

	private void synchronizeSlave(final SyncHostInfo info) {

		logger.log(Level.INFO, "Establishing initial replication.");

		try (final Tx tx = StructrApp.getInstance().tx()) {

			CloudService.doRemote(SecurityContext.getSuperUserInstance(), new UpdateTransmission(), info, new LoggingListener());

			tx.success();

		} catch (Throwable t) {
			logger.log(Level.WARNING, "", t);
		}

		logger.log(Level.INFO, "Done.");
	}

	// ----- nested classes -----
	private static class SyncListener implements CloudListener {

		private int successCount         = 0;
		private int requiredSuccessCount = 0;

		public SyncListener(final int requiredSuccessCount) {
			this.requiredSuccessCount = requiredSuccessCount;
		}

		@Override
		public void transmissionStarted() {
		}

		@Override
		public void transmissionFinished() {
			successCount++;
		}

		@Override
		public void transmissionAborted() {
		}

		@Override
		public void transmissionProgress(final String message) {
		}

		public boolean wasSuccessful() {
			return successCount >= requiredSuccessCount;
		}
	}

	private static class SyncHostInfo implements CloudHost {

		private ReplicationStatus status = null;
		private String instanceId        = null;
		private String host              = null;
		private String user              = null;
		private String pwd               = null;
		private int port                 = -1;

		public SyncHostInfo(final String host, final String user, final String pwd, final String portSource) {

			this.host = host;
			this.user = user;
			this.pwd  = pwd;
			this.port = Integer.valueOf(portSource);
		}

		@Override
		public String toString() {
			return host + ":" + port;
		}

		@Override
		public String getHostName() {
			return host;
		}

		@Override
		public String getUserName() {
			return user;
		}

		@Override
		public String getPassword() {
			return pwd;
		}

		@Override
		public int getPort() {
			return port;
		}

		public void setReplicationStatus(final ReplicationStatus status) {

			this.instanceId = status.getSlaveId();
			this.status     = status;
		}

		public long getLastSyncTimestamp() {
			return status.getLastSync();
		}

		public String getInstanceId() {
			return instanceId;
		}
	}

	private class LoggingListener implements CloudListener {

		@Override
		public void transmissionStarted() {
			logger.log(Level.INFO, "Transmission started");
		}

		@Override
		public void transmissionFinished() {
			logger.log(Level.INFO, "Transmission finished");
		}

		@Override
		public void transmissionAborted() {
			logger.log(Level.INFO, "Transmission aborted");
		}

		@Override
		public void transmissionProgress(final String message) {
			logger.log(Level.INFO, "Transmission progress {0}", message );
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy