[hue] Fix bug due to parallel PUT commands (#15324)
Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
parent
3b9b0236b4
commit
297640aa77
@ -399,6 +399,41 @@ public class Clip2Bridge implements Closeable {
|
|||||||
ACTIVE;
|
ACTIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for throttling HTTP GET and PUT requests to prevent overloading the Hue bridge.
|
||||||
|
* <p>
|
||||||
|
* The Hue Bridge can get confused if they receive too many HTTP requests in a short period of time (e.g. on start
|
||||||
|
* up), or if too many HTTP sessions are opened at the same time, which cause it to respond with an HTML error page.
|
||||||
|
* So this class a) waits to acquire permitCount (or no more than MAX_CONCURRENT_SESSIONS) stream permits, and b)
|
||||||
|
* throttles the requests to a maximum of one per REQUEST_INTERVAL_MILLISECS.
|
||||||
|
*/
|
||||||
|
private class Throttler implements AutoCloseable {
|
||||||
|
private final int permitCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param permitCount indicates how many stream permits to be acquired.
|
||||||
|
* @throws InterruptedException
|
||||||
|
*/
|
||||||
|
Throttler(int permitCount) throws InterruptedException {
|
||||||
|
this.permitCount = permitCount;
|
||||||
|
streamMutex.acquire(permitCount);
|
||||||
|
long delay;
|
||||||
|
synchronized (Clip2Bridge.this) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
delay = lastRequestTime
|
||||||
|
.map(t -> Math.max(0, Duration.between(now, t).toMillis() + REQUEST_INTERVAL_MILLISECS))
|
||||||
|
.orElse(0L);
|
||||||
|
lastRequestTime = Optional.of(now.plusMillis(delay));
|
||||||
|
}
|
||||||
|
Thread.sleep(delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
streamMutex.release(permitCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(Clip2Bridge.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(Clip2Bridge.class);
|
||||||
|
|
||||||
private static final String APPLICATION_ID = "org-openhab-binding-hue-clip2";
|
private static final String APPLICATION_ID = "org-openhab-binding-hue-clip2";
|
||||||
@ -662,11 +697,10 @@ public class Clip2Bridge implements Closeable {
|
|||||||
if (Objects.isNull(session) || session.isClosed()) {
|
if (Objects.isNull(session) || session.isClosed()) {
|
||||||
throw new ApiException("HTTP 2 session is null or closed");
|
throw new ApiException("HTTP 2 session is null or closed");
|
||||||
}
|
}
|
||||||
throttle();
|
try (Throttler throttler = new Throttler(1)) {
|
||||||
String url = getUrl(reference);
|
String url = getUrl(reference);
|
||||||
HeadersFrame headers = prepareHeaders(url, MediaType.APPLICATION_JSON);
|
LOGGER.trace("GET {} HTTP/2", url);
|
||||||
LOGGER.trace("GET {} HTTP/2", url);
|
HeadersFrame headers = prepareHeaders(url, MediaType.APPLICATION_JSON);
|
||||||
try {
|
|
||||||
Completable<@Nullable Stream> streamPromise = new Completable<>();
|
Completable<@Nullable Stream> streamPromise = new Completable<>();
|
||||||
ContentStreamListenerAdapter contentStreamListener = new ContentStreamListenerAdapter();
|
ContentStreamListenerAdapter contentStreamListener = new ContentStreamListenerAdapter();
|
||||||
session.newStream(headers, streamPromise, contentStreamListener);
|
session.newStream(headers, streamPromise, contentStreamListener);
|
||||||
@ -696,8 +730,6 @@ public class Clip2Bridge implements Closeable {
|
|||||||
throw new ApiException("Error sending request", e);
|
throw new ApiException("Error sending request", e);
|
||||||
} catch (TimeoutException e) {
|
} catch (TimeoutException e) {
|
||||||
throw new ApiException("Error sending request", e);
|
throw new ApiException("Error sending request", e);
|
||||||
} finally {
|
|
||||||
throttleDone();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -917,14 +949,15 @@ public class Clip2Bridge implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use an HTTP/2 PUT command to send a resource to the server. Note: the Hue bridge server can sometimes get
|
* Use an HTTP/2 PUT command to send a resource to the server. Note: the Hue Bridge can get confused by parallel
|
||||||
* confused by parallel PUT commands, so use 'synchronized' to prevent that.
|
* overlapping PUT resp. GET commands which cause it to respond with an HTML error page. So this method acquires all
|
||||||
|
* of the stream access permits (given by MAX_CONCURRENT_STREAMS) in order to prevent such overlaps.
|
||||||
*
|
*
|
||||||
* @param resource the resource to put.
|
* @param resource the resource to put.
|
||||||
* @throws ApiException if something fails.
|
* @throws ApiException if something fails.
|
||||||
* @throws InterruptedException
|
* @throws InterruptedException
|
||||||
*/
|
*/
|
||||||
public synchronized void putResource(Resource resource) throws ApiException, InterruptedException {
|
public void putResource(Resource resource) throws ApiException, InterruptedException {
|
||||||
if (onlineState == State.CLOSED) {
|
if (onlineState == State.CLOSED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -932,14 +965,13 @@ public class Clip2Bridge implements Closeable {
|
|||||||
if (Objects.isNull(session) || session.isClosed()) {
|
if (Objects.isNull(session) || session.isClosed()) {
|
||||||
throw new ApiException("HTTP 2 session is null or closed");
|
throw new ApiException("HTTP 2 session is null or closed");
|
||||||
}
|
}
|
||||||
throttle();
|
try (Throttler throttler = new Throttler(MAX_CONCURRENT_STREAMS)) {
|
||||||
String requestJson = jsonParser.toJson(resource);
|
String requestJson = jsonParser.toJson(resource);
|
||||||
ByteBuffer requestBytes = ByteBuffer.wrap(requestJson.getBytes(StandardCharsets.UTF_8));
|
ByteBuffer requestBytes = ByteBuffer.wrap(requestJson.getBytes(StandardCharsets.UTF_8));
|
||||||
String url = getUrl(new ResourceReference().setId(resource.getId()).setType(resource.getType()));
|
String url = getUrl(new ResourceReference().setId(resource.getId()).setType(resource.getType()));
|
||||||
HeadersFrame headers = prepareHeaders(url, MediaType.APPLICATION_JSON, "PUT", requestBytes.capacity(),
|
HeadersFrame headers = prepareHeaders(url, MediaType.APPLICATION_JSON, "PUT", requestBytes.capacity(),
|
||||||
MediaType.APPLICATION_JSON);
|
MediaType.APPLICATION_JSON);
|
||||||
LOGGER.trace("PUT {} HTTP/2 >> {}", url, requestJson);
|
LOGGER.trace("PUT {} HTTP/2 >> {}", url, requestJson);
|
||||||
try {
|
|
||||||
Completable<@Nullable Stream> streamPromise = new Completable<>();
|
Completable<@Nullable Stream> streamPromise = new Completable<>();
|
||||||
ContentStreamListenerAdapter contentStreamListener = new ContentStreamListenerAdapter();
|
ContentStreamListenerAdapter contentStreamListener = new ContentStreamListenerAdapter();
|
||||||
session.newStream(headers, streamPromise, contentStreamListener);
|
session.newStream(headers, streamPromise, contentStreamListener);
|
||||||
@ -964,8 +996,6 @@ public class Clip2Bridge implements Closeable {
|
|||||||
}
|
}
|
||||||
} catch (ExecutionException | TimeoutException e) {
|
} catch (ExecutionException | TimeoutException e) {
|
||||||
throw new ApiException("putResource() error sending request", e);
|
throw new ApiException("putResource() error sending request", e);
|
||||||
} finally {
|
|
||||||
throttleDone();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1037,30 +1067,4 @@ public class Clip2Bridge implements Closeable {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hue Bridges get confused if they receive too many HTTP requests in a short period of time (e.g. on start up), or
|
|
||||||
* if too many HTTP sessions are opened at the same time. So this method throttles the requests to a maximum of one
|
|
||||||
* per REQUEST_INTERVAL_MILLISECS, and ensures that no more than MAX_CONCURRENT_SESSIONS sessions are started.
|
|
||||||
*
|
|
||||||
* @throws InterruptedException
|
|
||||||
*/
|
|
||||||
private synchronized void throttle() throws InterruptedException {
|
|
||||||
streamMutex.acquire();
|
|
||||||
Instant now = Instant.now();
|
|
||||||
if (lastRequestTime.isPresent()) {
|
|
||||||
long delay = Duration.between(now, lastRequestTime.get()).toMillis() + REQUEST_INTERVAL_MILLISECS;
|
|
||||||
if (delay > 0) {
|
|
||||||
Thread.sleep(delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastRequestTime = Optional.of(now);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release the mutex.
|
|
||||||
*/
|
|
||||||
private void throttleDone() {
|
|
||||||
streamMutex.release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user