package com.workshare.msnos.core; import static com.workshare.msnos.core.Message.Type.PRS; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import net.jodah.expiringmap.ExpiringMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.workshare.msnos.core.MsnosException.Code; import com.workshare.msnos.core.cloud.AgentWatchdog; import com.workshare.msnos.core.cloud.IdentifiablesList; import com.workshare.msnos.core.cloud.IdentifiablesList.Callback; import com.workshare.msnos.core.cloud.MessageValidators; import com.workshare.msnos.core.cloud.Multicaster; import com.workshare.msnos.core.payloads.FltPayload; import com.workshare.msnos.core.payloads.Presence; import com.workshare.msnos.core.protocols.ip.Endpoint; import com.workshare.msnos.core.protocols.ip.HttpEndpoint; import com.workshare.msnos.core.protocols.ip.http.HttpGateway; import com.workshare.msnos.core.receipts.SingleReceipt; import com.workshare.msnos.core.routing.Router; import com.workshare.msnos.core.security.Signer; import com.workshare.msnos.soup.ShutdownHooks; import com.workshare.msnos.soup.ShutdownHooks.Hook; import com.workshare.msnos.soup.json.Json; import com.workshare.msnos.soup.threading.ExecutorServices; public class Cloud implements Identifiable { private static final Long ENQUIRY_EXPIRE = Long.getLong("com.ws.msnos.agent.enquiry.timeout", 30); private static final ScheduledExecutorService DEFAULT_SCHEDULER = ExecutorServices.newSingleThreadScheduledExecutor(); private static final Logger log = LoggerFactory.getLogger(Cloud.class); public static interface Listener { public void onMessage(Message message); } private final Iden iden; private final String signid; private final IdentifiablesList<LocalAgent> localAgents; private final IdentifiablesList<RemoteAgent> remoteAgents; private final IdentifiablesList<RemoteEntity> remoteClouds; transient private final Set<Gateway> gates; transient private final Ring ring; transient private final Signer signer; transient private final Internal internal; transient private final Sender sender; transient private final Receiver receiver; transient private final Map<UUID, Iden> enquiries; transient private final MessageValidators validators; public class Internal { public IdentifiablesList<LocalAgent> localAgents() { return localAgents; } public IdentifiablesList<RemoteAgent> remoteAgents() { return remoteAgents; } public IdentifiablesList<RemoteEntity> remoteClouds() { return remoteClouds; } public Message sign(Message message) { return Cloud.this.sign(message); } public Cloud cloud() { return Cloud.this; } } public Cloud(UUID uuid) throws MsnosException { this(uuid, null); } public Cloud(UUID uuid, String signid) throws MsnosException { this(uuid, signid, Gateways.all()); } public Cloud(UUID uuid, String signid, Set<Gateway> gates) { this(uuid, signid, new Signer(), null, null, gates, new Multicaster(), DEFAULT_SCHEDULER); } Cloud(final UUID uuid, String signid, Signer signer, Sender sender, Receiver receiver, Set<Gateway> gates, Multicaster multicaster, ScheduledExecutorService executor) { this.iden = new Iden(Iden.Type.CLD, uuid); this.enquiries = ExpiringMap.builder().expiration(ENQUIRY_EXPIRE, TimeUnit.SECONDS).build(); this.localAgents = new IdentifiablesList<LocalAgent>(); this.remoteAgents = new IdentifiablesList<RemoteAgent>(onRemoteAgentsChange()); this.remoteClouds = new IdentifiablesList<RemoteEntity>(); this.gates = Collections.unmodifiableSet(gates); this.internal = new Internal(); this.signer = signer; this.signid = signid; this.ring = calculateRing(gates); this.validators = new MessageValidators(this.internal); final Router router = new Router(this, gates); this.sender = (sender != null) ? sender : new Sender(router); this.receiver = (receiver != null) ? receiver : new Receiver(this, gates, multicaster, router); addShutdownHook(uuid); startAgentWatchdog(executor); } @Override public String toString() { try { return Json.toJsonString(this); } catch (Throwable any) { return super.toString(); } } @Override public Iden getIden() { return iden; } public MessageValidators validators() { return validators; } public boolean containsAgent(Iden iden) { return remoteAgents.containsKey(iden) || containsLocalAgent(iden); } public boolean containsLocalAgent(Iden iden) { return localAgents.containsKey(iden); } public RemoteAgent getRemoteAgent(final Iden iden) { return remoteAgents.get(iden); } public LocalAgent getLocalAgent(final Iden iden) { return localAgents.get(iden); } public Collection<RemoteAgent> getRemoteAgents() { return remoteAgents.list(); } public Collection<LocalAgent> getLocalAgents() { return localAgents.list(); } public Set<Gateway> getGateways() { return gates; } public Receipt send(Message message) throws MsnosException { checkCloudAlive(); return sender.send(this, sign(message)); } public Receipt sendSync(Message message) throws MsnosException { checkCloudAlive(); final SingleReceipt receipt = SingleReceipt.unknown(message); sender.sendSync(this, sign(message), receipt); return receipt; } void onJoin(LocalAgent agent) throws MsnosException { checkCloudAlive(); log.debug("Local agent joined: {}", agent); localAgents.add(agent); Receipt receipt = sendSync(new MessageBuilder(Message.Type.PRS, agent, this).with(new Presence(true, agent)).make()); waitForDelivery(receipt, 1, TimeUnit.SECONDS); sendSync(new MessageBuilder(Message.Type.DSC, agent, this).make()); } private void waitForDelivery(Receipt receipt, final int amount, final TimeUnit unit) throws MsnosException { try { receipt.waitForDelivery(amount, unit); } catch (InterruptedException e) { Thread.interrupted(); throw new MsnosException(e.getMessage(), Code.SEND_FAILED); } } void onLeave(LocalAgent agent) throws MsnosException { checkCloudAlive(); sendSync(new MessageBuilder(Message.Type.PRS, agent, this).with(new Presence(false, agent)).make()); log.debug("Local agent left: {}", agent); localAgents.remove(agent.getIden()); } private void checkCloudAlive() throws MsnosException { if (gates.size() == 0) throw new MsnosException("This cloud is not connected as it is a mirror of a remote one", MsnosException.Code.NOT_CONNECTED); } public Listener addListener(com.workshare.msnos.core.Cloud.Listener listener) { return receiver.caster().addListener(listener); } public void removeListener(com.workshare.msnos.core.Cloud.Listener listener) { receiver.caster().removeListener(listener); } public Listener addSynchronousListener(com.workshare.msnos.core.Cloud.Listener listener) { return receiver.caster().addSynchronousListener(listener); } private void enquiryAgentIfNecessary(Message message) { final Iden from = message.getFrom(); if (from.getType() == Iden.Type.AGT && message.getType() != PRS) { if (!remoteAgents.containsKey(from)) if (enquiries.get(from.getUUID()) == null) { enquiries.put(from.getUUID(), from); try { log.info("Enquiring unknown agent {}", from); final Cloud cloud = internal.cloud(); cloud.send(new MessageBuilder(Message.Type.DSC, cloud, from).make()); } catch (IOException e) { log.error("Unexpected exception sending message " + message, e); } } } } void postProcess(Message message) { enquiryAgentIfNecessary(message); final Iden from = message.getFrom(); touch(remoteAgents.get(from)); touch(remoteClouds.get(from)); } private void touch(RemoteEntity entity) { if (entity != null) entity.touch(); } public void removeFaultyAgent(RemoteEntity agent) { log.warn("Removing faulty agent " + agent); RemoteEntity result = remoteAgents.remove(agent.getIden()); if (result != null) receiver.caster().dispatch(new MessageBuilder(Message.Type.FLT, this, this).with(new FltPayload(agent.getIden())).make()); } public void registerLocalMsnosEndpoint(HttpEndpoint endpoint) { log.debug("Registering local HTTP endpoint: {}", endpoint); LocalAgent agent = localAgents.get(endpoint.getTarget()); if (agent == null) { log.warn("Weird... a local agent registered an httpendpoint, but I do not know him"); return; } agent.registerEndpoint(endpoint); log.debug("Agent {} updated, added endpoints {}", agent.getIden().getUUID(), endpoint); } public void unregisterRemoteMsnosEndpoint(HttpEndpoint endpoint) throws MsnosException { log.debug("Unregistering remote HTTP endpoint: {}", endpoint); unregisterFromHttpGateway(endpoint); } private void unregisterFromHttpGateway(HttpEndpoint endpoint) throws MsnosException { for (Gateway gate : gates) { if (gate instanceof HttpGateway) { gate.endpoints().remove(endpoint); } } } public void registerRemoteMsnosEndpoint(HttpEndpoint endpoint) throws MsnosException { log.debug("Registering remote HTTP endpoint: {}", endpoint); registerOnHttpGateway(endpoint); registerOnRemoteAgent(endpoint); } private void registerOnHttpGateway(HttpEndpoint endpoint) throws MsnosException { for (Gateway gate : gates) { if (gate instanceof HttpGateway) { gate.endpoints().install(endpoint); } } } private void registerOnRemoteAgent(HttpEndpoint endpoint) { RemoteAgent agent = getRemoteAgent(endpoint.getTarget()); if (agent == null) { log.warn("Weird... a remote agent registered an httpendpoint, but I do not know him"); return; } if (agent.getEndpoints(Endpoint.Type.HTTP).contains(endpoint)) return; agent.update(newEndpoints(endpoint, agent)); log.debug("Agent {} updated, new endpoints are {}", agent, newEndpoints(endpoint, agent)); } private Set<Endpoint> newEndpoints(HttpEndpoint endpoint, Agent agent) { Set<Endpoint> endpoints = new HashSet<Endpoint>(); endpoints.addAll(agent.getEndpoints()); endpoints.add(endpoint); return endpoints; } private Message sign(Message message) { if (signid == null) return message; try { return signer.signed(message, signid); } catch (IOException e) { log.warn("Failed to sign message {} using key {}", message, signid); return message; } } public RemoteAgent find(final Iden iden) { RemoteAgent remoteAgent = null; for (RemoteAgent agent : getRemoteAgents()) { if (agent.getIden().equals(iden)) { remoteAgent = agent; break; } } return remoteAgent; } public void process(Message message, String gateName) { receiver.process(message, gateName); } public Ring getRing() { return ring; } Internal internal() { return internal; } private void startAgentWatchdog(ScheduledExecutorService executor) { new AgentWatchdog(this, executor).start(); } private Ring calculateRing(Set<Gateway> gateways) { HashSet<Endpoint> endpoints = new HashSet<Endpoint>(); for (Gateway gate : gateways) { final Set<? extends Endpoint> points = gate.endpoints().all(); if (points != null) endpoints.addAll(points); } return Ring.make(endpoints); } private void addShutdownHook(final UUID uuid) { ShutdownHooks.addHook(new Hook() { @Override public void run() { log.info("Asking all agents to leave..."); try { for (LocalAgent agent : localAgents.list()) { ensureLeft(agent); } } finally { log.info("done!"); } } private void ensureLeft(LocalAgent agent) { if (agent.getCloud() != null) try { log.debug("- agent {} is leaving...", agent.getIden().getUUID()); agent.leave(); } catch (Throwable ex) { log.warn("Unexpected exception while enforcing agent to leave the cloud", ex); } } @Override public String name() { return "Ensure all agents left the cloud " + uuid; } @Override public int priority() { return 0; } }); } private Callback<RemoteAgent> onRemoteAgentsChange() { return new Callback<RemoteAgent>() { @Override public void onAdd(RemoteAgent agent) { for(Endpoint endpoint : agent.getEndpoints(Endpoint.Type.HTTP)) { try { registerRemoteMsnosEndpoint((HttpEndpoint) endpoint); } catch (Exception e) { log.error("wtf - unable to register http endpoint", e); } } } @Override public void onRemove(RemoteAgent agent) { for(Endpoint endpoint : agent.getEndpoints(Endpoint.Type.HTTP)) { try { unregisterRemoteMsnosEndpoint((HttpEndpoint) endpoint); } catch (Exception e) { log.error("wtf - unable to register http endpoint", e); } } }}; } }