private static final int CHECK_ALIVE_SECONDS = 300;
private static final int REQUEST_INTERVAL_MILLISECS = 50;
private static final int MAX_CONCURRENT_STREAMS = 3;
- private static final int RESTART_AFTER_SECONDS = 5;
private static final ResourceReference BRIDGE = new ResourceReference().setType(ResourceType.BRIDGE);
private final Semaphore streamMutex = new Semaphore(MAX_CONCURRENT_STREAMS, true);
private boolean closing;
- private boolean internalRestartScheduled;
- private boolean externalRestartScheduled;
private State onlineState = State.CLOSED;
private Optional<Instant> lastRequestTime = Optional.empty();
private Instant sessionExpireTime = Instant.MAX;
private @Nullable Session http2Session;
private @Nullable Future<?> checkAliveTask;
- private @Nullable Future<?> internalRestartTask;
private Map<Integer, Future<?>> fatalErrorTasks = new ConcurrentHashMap<>();
/**
* @param bridgeHandler the bridge handler.
* @param hostName the host name (ip address) of the Hue bridge
* @param applicationKey the application key.
+ * @throws ApiException if unable to open Jetty HTTP/2 client.
*/
public Clip2Bridge(HttpClientFactory httpClientFactory, Clip2BridgeHandler bridgeHandler, String hostName,
- String applicationKey) {
+ String applicationKey) throws ApiException {
LOGGER.debug("Clip2Bridge()");
httpClient = httpClientFactory.getCommonHttpClient();
http2Client = httpClientFactory.createHttp2Client("hue-clip2", httpClient.getSslContextFactory());
http2Client.setConnectTimeout(Clip2Bridge.TIMEOUT_SECONDS * 1000);
http2Client.setIdleTimeout(-1);
+ try {
+ http2Client.start();
+ } catch (Exception e) {
+ throw new ApiException("Error starting HTTP/2 client", e);
+ }
this.bridgeHandler = bridgeHandler;
this.hostName = hostName;
this.applicationKey = applicationKey;
@Override
public void close() {
closing = true;
- externalRestartScheduled = false;
- internalRestartScheduled = false;
close2();
+ try {
+ http2Client.stop();
+ } catch (Exception e) {
+ }
}
/**
private void close2() {
synchronized (this) {
LOGGER.debug("close2()");
- boolean notifyHandler = onlineState == State.ACTIVE && !internalRestartScheduled
- && !externalRestartScheduled && !closing;
+ boolean notifyHandler = onlineState == State.ACTIVE && !closing;
onlineState = State.CLOSED;
synchronized (fatalErrorTasks) {
fatalErrorTasks.values().forEach(task -> cancelTask(task, true));
fatalErrorTasks.clear();
}
- if (!internalRestartScheduled) {
- // don't close the task if a restart is current
- cancelTask(internalRestartTask, true);
- internalRestartTask = null;
- }
cancelTask(checkAliveTask, true);
checkAliveTask = null;
closeSession();
- try {
- http2Client.stop();
- } catch (Exception e) {
- // ignore
- }
if (notifyHandler) {
bridgeHandler.onConnectionOffline();
}
private void closeSession() {
LOGGER.debug("closeSession()");
Session session = http2Session;
- if (Objects.nonNull(session)) {
+ if (Objects.nonNull(session) && !session.isClosed()) {
session.close(0, null, Callback.NOOP);
}
http2Session = null;
* @param cause the exception that caused the error.
*/
private synchronized void fatalError(Object listener, Http2Exception cause) {
- if (externalRestartScheduled || internalRestartScheduled || onlineState == State.CLOSED || closing) {
+ if (onlineState == State.CLOSED || closing) {
return;
}
String causeId = listener.getClass().getSimpleName();
if (listener instanceof ContentStreamListenerAdapter) {
// on GET / PUT requests the caller handles errors and closes the stream; the session is still OK
LOGGER.debug("fatalError() {} {} ignoring", causeId, cause.error);
- } else if (cause.error == Http2Error.GO_AWAY) {
- LOGGER.debug("fatalError() {} {} scheduling reconnect", causeId, cause.error);
-
- // schedule task to open again
- internalRestartScheduled = true;
- cancelTask(internalRestartTask, false);
- internalRestartTask = bridgeHandler.getScheduler().schedule(
- () -> internalRestart(onlineState == State.ACTIVE), RESTART_AFTER_SECONDS, TimeUnit.SECONDS);
-
- // force close immediately to be clean when internalRestart() starts
- close2();
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("fatalError() {} {} closing", causeId, cause.error, cause);
* @throws InterruptedException
*/
public Resources getResources(ResourceReference reference) throws ApiException, InterruptedException {
- sleepDuringRestart();
if (onlineState == State.CLOSED) {
throw new ApiException("getResources() offline");
}
return Objects.isNull(id) || id.isEmpty() ? url : url + "/" + id;
}
- /**
- * Restart the session.
- *
- * @param active boolean that selects whether to restart in active or passive mode.
- */
- private void internalRestart(boolean active) {
- try {
- openPassive();
- if (active) {
- openActive();
- }
- internalRestartScheduled = false;
- } catch (ApiException e) {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("internalRestart() failed", e);
- } else {
- LOGGER.warn("Scheduled reconnection task failed.");
- }
- internalRestartScheduled = false;
- close2();
- } catch (InterruptedException e) {
- }
- }
-
/**
* The event stream calls this method when it has received text data. It parses the text as JSON into a list of
* Event entries, converts the list of events to a list of resources, and forwards that list to the bridge
synchronized (this) {
LOGGER.debug("openPassive()");
onlineState = State.CLOSED;
- try {
- http2Client.start();
- } catch (Exception e) {
- throw new ApiException("Error starting HTTP/2 client", e);
- }
openSession();
openCheckAliveTask();
onlineState = State.PASSIVE;
}
/**
- * Use an HTTP/2 PUT command to send a resource to the server.
+ * Use an HTTP/2 PUT command to send a resource to the server. Note: the Hue bridge server can sometimes get
+ * confused by parallel PUT commands, so use 'synchronized' to prevent that.
*
* @param resource the resource to put.
* @throws ApiException if something fails.
* @throws InterruptedException
*/
- public void putResource(Resource resource) throws ApiException, InterruptedException {
- sleepDuringRestart();
+ public synchronized void putResource(Resource resource) throws ApiException, InterruptedException {
if (onlineState == State.CLOSED) {
return;
}
throw new HttpUnauthorizedException("Application key registration failed");
}
- public void setExternalRestartScheduled() {
- externalRestartScheduled = true;
- internalRestartScheduled = false;
- cancelTask(internalRestartTask, false);
- internalRestartTask = null;
- close2();
- }
-
- /**
- * Sleep the caller during any period when the connection is restarting.
- *
- * @throws ApiException if anything failed.
- * @throws InterruptedException
- */
- private void sleepDuringRestart() throws ApiException, InterruptedException {
- Future<?> restartTask = this.internalRestartTask;
- if (Objects.nonNull(restartTask)) {
- try {
- restartTask.get(RESTART_AFTER_SECONDS * 2, TimeUnit.SECONDS);
- } catch (ExecutionException | TimeoutException e) {
- throw new ApiException("sleepDuringRestart() error", e);
- }
- }
- internalRestartScheduled = false;
- }
-
/**
* Test the Hue Bridge connection state by attempting to connect and trying to execute a basic command that requires
* authentication.