/* * Copyright 2015 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package io.reactivex.netty.protocol.http.ws.client; import io.reactivex.netty.protocol.http.ws.WebSocketConnection; import rx.Observable; import rx.Observable.Operator; import rx.Subscriber; import rx.annotations.Experimental; import rx.functions.Action0; import rx.functions.Actions; import rx.functions.Func1; /** * An operator to cache a {@link WebSocketConnection} until it closes, upon which the source that re-creates an HTTP * upgrade request to get a fresh {@link WebSocketConnection} is subscribed, to refresh the stale connection in the * cache. * * A typical usage example for this operator is: * <pre> {@code HttpClient.newClient(socketAddress) .createGet("/ws") .requestWebSocketUpgrade() .map(WebSocketResponse::getWebSocketConnection) .nest() .lift(new OperatorCacheSingleWebsocketConnection()) } </pre> * * Since multiple subscriptions to {@link WebSocketResponse#getWebSocketConnection()} do not re-run the original HTTP * upgrade request, this operator expects the source {@code Observable} to be passed to it, so that on close of the * cached {@link WebSocketConnection}, it can re-subscribe to the original HTTP request and create a fresh connection. * This is the reason the above code uses {@link Observable#nest()} to get a reference to the source {@code Observable}. * * <h2> Cache liveness guarantees</h2> * * Although, this operator will make sure that when the cached connection has terminated, the next refresh will * re-subscribe to the source, there is no guarantee that a dead connection is never emitted from this operator as it * completely depends on the timing of when the connection terminates and when a new subscription arrives. The two * events can be concurrent and hence unpredictable. */ @Experimental public class OperatorCacheSingleWebsocketConnection implements Operator<WebSocketConnection, Observable<Observable<WebSocketConnection>>> { private boolean subscribedToSource; /*Guarded by this*/ private Observable<WebSocketConnection> cachedSource; /*Guarded by this*/ @Override public Subscriber<? super Observable<Observable<WebSocketConnection>>> call(final Subscriber<? super WebSocketConnection> subscriber) { return new Subscriber<Observable<Observable<WebSocketConnection>>>(subscriber) { private volatile boolean anItemEmitted; @Override public void onCompleted() { if (!anItemEmitted) { subscriber.onError(new IllegalStateException("No Observable emitted from source.")); } } @Override public void onError(Throwable e) { subscriber.onError(e); } @Override public void onNext(Observable<Observable<WebSocketConnection>> source) { anItemEmitted = true; /** * The idea below is for using a single cache {@code Observable} so that the cache operator can cache * the generated connection. However, when the cached connection is terminated, a new cached source * must be generated to be used for subsequent subscriptions. * As the only way to re-run the original HTTP upgrade request, to obtain a fresh connection, is to * subscribe to the {@code Observable<Observable<WebSocketConnection>>}, that is the reason the below * code uses a {@code flatmap} to transform {@code Observable<Observable<WebSocketConnection>>} to an * {@code Observable<WebSocketConnection>} and still keeping the ability to re-subscribe to the original * {@code Observable<Observable<WebSocketConnection>>}. */ final Observable<WebSocketConnection> _cachedSource; final Observable<WebSocketConnection> o = source.flatMap( new Func1<Observable<WebSocketConnection>, Observable<WebSocketConnection>>() { @Override public Observable<WebSocketConnection> call(Observable<WebSocketConnection> connSource) { /*This is for flatmap to subscribe to the nested {@code Observable<WebSocketConnection>}*/ return connSource; } }).map(new Func1<WebSocketConnection, WebSocketConnection>() { @Override public WebSocketConnection call(WebSocketConnection connection) { Observable<Void> lifecycle = connection.closeListener(); lifecycle = lifecycle.onErrorResumeNext(Observable.<Void>empty()) .doAfterTerminate(new Action0() { @Override public void call() { synchronized (OperatorCacheSingleWebsocketConnection.this) { // refresh the source on next subscribe subscribedToSource = false; } } }); subscriber.add(lifecycle.subscribe(Actions.empty())); return connection; } }).cache(); synchronized (OperatorCacheSingleWebsocketConnection.this) { if (!subscribedToSource) { subscribedToSource = true; /*From here on, all subscriptions will use the newly created cached source which on first subscription will re-run the original HTTP upgrade request and get a fresh WS connection*/ cachedSource = o; } _cachedSource = cachedSource; } _cachedSource.unsafeSubscribe(subscriber); } }; } }