package org.deephacks.westty.internal.sockjs; import org.deephacks.westty.cluster.ClusterListener; import org.deephacks.westty.cluster.DistributedMultiMap; import org.deephacks.westty.cluster.EntryEvent; import org.vertx.java.core.AsyncResult; import org.vertx.java.core.AsyncResultHandler; import org.vertx.java.core.eventbus.impl.ServerIDs; import org.vertx.java.core.eventbus.impl.SubsMap; import org.vertx.java.core.eventbus.impl.hazelcast.HazelcastServerID; import org.vertx.java.core.impl.BlockingAction; import org.vertx.java.core.impl.VertxInternal; import org.vertx.java.core.net.impl.ServerID; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * Modified version of HazelcastSubsMap by <a href="http://tfox.org">Tim Fox</a>. * * Purpose is to decouple hazelcast implementation from Vertx. */ public class WesttySubsMap implements SubsMap, ClusterListener<String, HazelcastServerID> { private final VertxInternal vertx; private final DistributedMultiMap<String, HazelcastServerID> map; public WesttySubsMap(String name, VertxInternal vertx, DistributedMultiMap<String, HazelcastServerID> map) { this.vertx = vertx; this.map = map; map.addEntryListener(this, true); } /* The Hazelcast near cache is very slow so we use our own one. Keeping it in sync is a little tricky. As entries are added or removed the EntryListener will be called but when the node joins the cluster it isn't provided the initial state via the EntryListener Therefore the first time get is called for a subscription we *always* get the subs from Hazelcast (this is what the initialised flag is for), then consider that the initial state. While the get is in progress the entry listener may be being called, so we merge any pre-existing entries so we don't lose any. Hazelcast doesn't seem to have any consistent way to get an initial state plus a stream of updates. */ private ConcurrentMap<String, ServerIDs> cache = new ConcurrentHashMap<>(); public void removeAllForServerID(final ServerID serverID, final AsyncResultHandler<Void> completionHandler) { new BlockingAction<Void>(vertx, completionHandler) { public Void action() throws Exception { for (Map.Entry<String, HazelcastServerID> entry : map.entrySet()) { HazelcastServerID hid = entry.getValue(); if (hid.serverID.equals(serverID)) { map.remove(entry.getKey(), hid); } } return null; } }.run(); } @Override public void put(final String subName, final ServerID serverID, final AsyncResultHandler<Void> completionHandler) { new BlockingAction<Void>(vertx, completionHandler) { public Void action() throws Exception { map.put(subName, new HazelcastServerID(serverID)); return null; } }.run(); } @Override public void get(final String subName, final AsyncResultHandler<ServerIDs> completionHandler) { ServerIDs entries = cache.get(subName); if (entries != null && entries.isInitialised()) { completionHandler.handle(new AsyncResult<>(entries)); } else { new BlockingAction<Collection<HazelcastServerID>>(vertx, new AsyncResultHandler<Collection<HazelcastServerID>>() { public void handle(AsyncResult<Collection<HazelcastServerID>> result) { AsyncResult<ServerIDs> sresult; if (result.succeeded()) { Collection<HazelcastServerID> entries = result.result; ServerIDs sids; if (entries != null) { sids = new ServerIDs(entries.size()); for (HazelcastServerID hid : entries) { sids.add(hid.serverID); } } else { sids = new ServerIDs(0); } ServerIDs prev = cache.putIfAbsent(subName, sids); if (prev != null) { // Merge them prev.merge(sids); sids = prev; } sids.setInitialised(); sresult = new AsyncResult<>(sids); } else { sresult = new AsyncResult<>(result.exception); } completionHandler.handle(sresult); } }) { public Collection<HazelcastServerID> action() throws Exception { return map.get(subName); } }.run(); } } @Override public void remove(final String subName, final ServerID serverID, final AsyncResultHandler<Boolean> completionHandler) { new BlockingAction<Boolean>(vertx, completionHandler) { public Boolean action() throws Exception { return map.remove(subName, new HazelcastServerID(serverID)); } }.run(); } @Override public void entryAdded(EntryEvent<String, HazelcastServerID> entry) { addEntry(entry.getKey(), entry.getValue().serverID); } private void addEntry(String key, ServerID value) { ServerIDs entries = cache.get(key); if (entries == null) { entries = new ServerIDs(1); ServerIDs prev = cache.putIfAbsent(key, entries); if (prev != null) { entries = prev; } } entries.add(value); } @Override public void entryRemoved(EntryEvent<String, HazelcastServerID> entry) { removeEntry(entry.getKey(), entry.getValue().serverID); } private void removeEntry(String key, ServerID value) { ServerIDs entries = cache.get(key); if (entries != null) { entries.remove(value); if (entries.isEmpty()) { cache.remove(key); } } } @Override public void entryUpdated(EntryEvent<String, HazelcastServerID> entry) { String key = entry.getKey(); ServerIDs entries = cache.get(key); if (entries != null) { entries.add(entry.getValue().serverID); } } @Override public void entryEvicted(EntryEvent<String, HazelcastServerID> entry) { entryRemoved(entry); } }