package org.cloudname.backends.consul; import org.json.JSONException; import org.json.JSONObject; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** * Session entity sent to/from Consul. Uses a thread to keep the session alive until it is closed * or the JVM terminates. * * @author stalehd@gmail.com */ public class ConsulSession { private final String endpoint; private final String id; private final String name; private final int ttl; private final int lockDelay; private static final Logger LOG = Logger.getLogger(ConsulSession.class.getName()); private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private final Client httpClient = ClientBuilder.newClient(); private final String behavior = "delete"; private final AtomicBoolean closed = new AtomicBoolean(false); /** * Create new session object. * * @param endpoint Consul Agent endpoint to use * @param id The session ID. This is not validated * @param name Session name. Not used for anything but human readable tag for the session. * @param ttl Session TTL in seconds. The session is refreshed every TTL/2 seconds. * @param lockDelay Internal parameter for Consul that tells how often locks can be acquired * (and reacquired). In seconds. */ public ConsulSession(final String endpoint, final String id, final String name, final int ttl, final int lockDelay) { this.endpoint = endpoint; this.id = id; this.name = name; this.ttl = ttl * 1000; this.lockDelay = lockDelay; } /** * The session ID. */ public String getId() { return id; } /** * Session object that can be submitted to the Consul Agent. */ public String toJson() { return new JSONObject() .put("Name", id) .put("TTL", (ttl / 1000) + "s") .put("LockDelay", lockDelay + "s") .put("Behavior", behavior) .toString(); } /** * Build actual session from Consul Agent response. Only the ID field is returned, use the * submitted entity. */ public static ConsulSession fromJsonResponse(final ConsulSession submitted, final String json) { try { final JSONObject ret = new JSONObject(json); // Note that the TTL returned is in milliseconds. Nice gotcha. return new ConsulSession(submitted.endpoint, ret.getString("ID"), submitted.name, submitted.ttl / 1_000, submitted.lockDelay); } catch (final JSONException je) { LOG.log(Level.WARNING, "Couldn't grok JSON from Consul Agent. Response was " + json); return null; } } /** * Start the session refresh thread. */ public void startKeepAlive() { final Entity<String> emptyEntity = Entity.entity("{}", MediaType.APPLICATION_JSON_TYPE); try { executor.scheduleAtFixedRate(() -> { final Response response = httpClient .target(endpoint) .path("/v1/session/renew") .path(id).request() .put(emptyEntity); if (response.getStatus() != Response.Status.OK.getStatusCode()) { LOG.log(Level.WARNING, "Got " + response.getStatus() + " from Consul Agent when renewing sessions, exepected 200"); } }, ttl / 2, ttl / 2, TimeUnit.MILLISECONDS); } catch (final RejectedExecutionException re) { // This will be thrown if the task is cancelled before it is started. } } /** * @return true if the session is closed. */ public boolean isClosed() { return closed.get(); } /** * Close and remove session from Consul. This will remove all KV entries tagged with this * session. */ public void close() { executor.shutdownNow(); final Entity<String> emptyEntity = Entity.entity("{}", MediaType.APPLICATION_JSON_TYPE); final Response response = httpClient .target(endpoint) .path("/v1/session/destroy") .path(id) .request() .put(emptyEntity); if (response.getStatus() != Response.Status.OK.getStatusCode()) { LOG.log(Level.WARNING, "Got " + response.getStatus() + " from Consult Agent when removing session, expected 200"); } LOG.info("Removed session with ID " + id); closed.set(true); } }