package com.workshare.msnos.core.protocols.ip.www; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.util.EnumSet; import java.util.HashSet; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.HttpHostConnectException; import org.apache.http.entity.ContentType; import org.apache.http.entity.InputStreamEntity; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.workshare.msnos.core.Cloud; import com.workshare.msnos.core.Gateway; import com.workshare.msnos.core.Identifiable; import com.workshare.msnos.core.Message; import com.workshare.msnos.core.Message.Status; import com.workshare.msnos.core.Receipt; import com.workshare.msnos.core.protocols.ip.BaseEndpoint; import com.workshare.msnos.core.protocols.ip.Endpoints; import com.workshare.msnos.core.protocols.ip.www.WWWSynchronizer.Processor; import com.workshare.msnos.core.receipts.SingleReceipt; import com.workshare.msnos.core.serializers.WireSerializer; import com.workshare.msnos.soup.threading.ConcurrentBuildingMap; import com.workshare.msnos.soup.threading.ConcurrentBuildingMap.Factory; import com.workshare.msnos.soup.threading.Multicaster; public class WWWGateway implements Gateway { private enum Sync { TX, RX }; public static final int MAX_TOTAL_CONSECUTIVE_ERRORS = Integer.getInteger("com.ws.nsnos.www.sync.max.consecutive.errors", 3); public static final String SYSP_SYNC_PERIOD = "com.ws.nsnos.www.sync.period.millis"; public static final String SYSP_ADDRESS = "com.ws.nsnos.www.address"; private static final UUID NULL = new UUID(0, 0); private static final UUID VOID = new UUID(0, 1); private static Logger log = LoggerFactory.getLogger(WWWGateway.class); private final ScheduledExecutorService scheduler; private final HttpClient client; private final WireSerializer serializer; private final Map<Cloud, UUID> cloudListeners; private final Map<Cloud, Queue<Message>> cloudMessages; private final Multicaster<Listener, Message> caster; private final WWWSynchronizer synchro; private final String urlRoot; private final String urlMsgs; private final AtomicInteger syncing = new AtomicInteger(0); private int consecutiveRxErrors; private volatile boolean logNextException = true; public WWWGateway(HttpClient client, ScheduledExecutorService scheduler, WireSerializer serializer, Multicaster<Listener, Message> caster) throws IOException { this(client, new WWWSynchronizer(caster), scheduler, serializer, caster); } WWWGateway(HttpClient client, WWWSynchronizer processor, ScheduledExecutorService scheduler, WireSerializer serializer, Multicaster<Listener, Message> caster) throws IOException { this.synchro = processor; this.client = client; this.caster = caster; this.scheduler = scheduler; this.serializer = serializer; this.cloudListeners = new ConcurrentHashMap<Cloud, UUID>(); this.cloudMessages = new ConcurrentBuildingMap<Cloud, Queue<Message>>(new Factory<Queue<Message>>() { @Override public Queue<Message> make() { return new ConcurrentLinkedQueue<Message>(); } }); long period = loadSyncPeriod(); this.scheduler.scheduleAtFixedRate(new Runnable() { @Override public void run() { sync(EnumSet.of(Sync.TX, Sync.RX)); } }, period, period, TimeUnit.MILLISECONDS); this.urlRoot = System.getProperty(SYSP_ADDRESS, "https://www.zapnos.org/"); this.urlMsgs = composeUrl("api/1.0/messages"); try { ping(client); } catch (HttpHostConnectException ex) { log.warn("Unable to ping WWW endpoint - {}", ex.getMessage()); } } @Override public String name() { return "WWW"; } public String root() { return this.urlRoot; } private void ping(HttpClient client) throws IOException, ClientProtocolException, MalformedURLException { HttpResponse response = client.execute(new HttpGet(composeUrl("ping"))); EntityUtils.consume(response.getEntity()); } private String composeUrl(String path) throws MalformedURLException { final String url = new URL(new URL(urlRoot), path).toExternalForm(); return url; } @Override public void close() throws IOException { sync(EnumSet.of(Sync.TX)); } @Override public void addListener(Cloud cloud, Listener listener) { cloudListeners.put(cloud, NULL); caster.addListener(listener); } @Override public Endpoints endpoints() { return BaseEndpoint.create(); } @Override public Receipt send(Cloud cloud, Message message, Identifiable to) throws IOException { cloudMessages.get(cloud).add(message); return new SingleReceipt(this, Status.PENDING, message); } private void sync(Set<Sync> syncs) { int value = syncing.incrementAndGet(); try { if (value > 1) { log.warn("Request to sync while syncing was in progress"); return; } else { doSync(syncs); } } finally { syncing.decrementAndGet(); } } private void doSync(Set<Sync> syncs) { if (syncs.contains(Sync.TX)) try { if (syncTx()) logNextException = true; } catch (HttpHostConnectException ex) { logIfNecessary(ex); return; } catch (Exception ex) { log.warn("Unexpected exception during sync (TX)", ex); } if (syncs.contains(Sync.RX)) try { syncRx(); noRxError(); logNextException = true; } catch (HttpHostConnectException ex) { onRxError(); logIfNecessary(ex); } catch (Exception ex) { onRxError(); log.warn("Unexpected exception during sync (RX)", ex); } } private void logIfNecessary(HttpHostConnectException ex) { if (logNextException) { log.warn("Unexpected exception while connecting to WWW gateway"); logNextException = false; } } private void noRxError() { consecutiveRxErrors = 0; } private void onRxError() { if (++consecutiveRxErrors >= MAX_TOTAL_CONSECUTIVE_ERRORS) { log.debug("Too many consecutive errors: resetting all gates!"); consecutiveRxErrors = 0; Set<Cloud> clouds = cloudListeners.keySet(); for (Cloud cloud : clouds) { cloudListeners.put(cloud, NULL); } } } private void syncRx() throws IOException { Set<Cloud> clouds = new HashSet<Cloud>(cloudListeners.keySet()); for (Cloud cloud : clouds) { final UUID uuid = cloudListeners.get(cloud); String url = urlMsgs + "?cloud=" + cloud.getIden().getUUID(); if (uuid != NULL && uuid != VOID) url += "&message=" + uuid; Processor processor = (uuid == NULL) ? synchro.init(cloud) : null; int total = 0; HttpGet request = new HttpGet(url); HttpResponse res = client.execute(request); try { BufferedReader in = new BufferedReader(new InputStreamReader(res.getEntity().getContent(), "UTF-8")); try { String line; Message last = null; while ((line = in.readLine()) != null) { Message msg = serializer.fromText(line, Message.class); if (msg != null) { ++total; if (processor != null) processor.accept(msg); else caster.dispatch(msg); last = msg; } } log.debug("last message read: {}", last); if (last != null) cloudListeners.put(cloud, last.getUuid()); else if (uuid == NULL) cloudListeners.put(cloud, VOID); } finally { in.close(); } } finally { EntityUtils.consume(res.getEntity()); } log.debug("Processed a total of {} messages", total); if (processor != null) processor.commit(); } } private boolean syncTx() throws IOException { if (cloudMessages.size() == 0) { log.debug("No messages to send so far"); return false; } boolean sent = false; Set<Cloud> clouds = new HashSet<Cloud>(cloudMessages.keySet()); for (Cloud cloud : clouds) { Queue<Message> messages = cloudMessages.get(cloud); if (messages.size() == 0) continue; HttpPost request = new HttpPost(urlMsgs + "?cloud=" + cloud.getIden().getUUID()); request.setEntity(toInputStreamEntity(messages)); HttpResponse res = client.execute(request); EntityUtils.consume(res.getEntity()); sent = true; } return sent; } private InputStreamEntity toInputStreamEntity(final Queue<Message> messages) { return new InputStreamEntity(new MessagesInputSream(serializer, messages), ContentType.TEXT_PLAIN); } private static Long loadSyncPeriod() { return Long.getLong(SYSP_SYNC_PERIOD, 5000L); } }