Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -428,12 +428,40 @@ public final void coreWrites(@NonNull byte[] data) {
@Override
public void coreAcksWrite(long n) { Log.d(LOG_DOMAIN, "%s.coreAckReceive: %d", this, n); }

// Core wants to break the connection
/**
* Core wants to break the connection.
*
* <p>C4SocketFactory.requestClose callback (kC4NoFraming mode). LiteCore
* calls this method once per socket to have us send the WebSocket close
* frame to the remote — by calling {@link SocketToRemote#closeRemote},
* which calls OkHttp's {@code ws.close()}. In a local close it sends
* our initial close; in a remote close it sends the echo for a close
* the remote initiated.
*
* <p>Local close (e.g. {@code replicator.stop()}):
* <pre>
* 1. LiteCore invokes this method directly.
* 2. ensureState advances OPEN → CLOSING; closeRemote sends the close frame.
* 3. Server echoes; OkHttp delivers it via remoteRequestsClose (no-op — state already CLOSING).
* 4. OkHttp → remoteClosed (CLOSING → CLOSED, c4socket_closed).
* </pre>
*
* <p>Remote close (remote-initiated):
* <pre>
* 1. Server sends close; OkHttp delivers it via remoteRequestsClose, which advances
* OPEN → CLOSING and notifies LiteCore (c4socket_closeRequested).
* 2. LiteCore drains in-flight work, then calls this method back.
* 3. ensureState sees state already CLOSING and proceeds; closeRemote sends the close echo.
* 4. OkHttp → remoteClosed (CLOSING → CLOSED, c4socket_closed).
* </pre>
*/
@Override
public final void coreRequestsClose(@NonNull CloseStatus status) {
Log.d(LOG_DOMAIN, "%s.coreRequestsClose: %s", this, status);

if (!changeState(SocketState.CLOSING)) { return; }
// State may already be CLOSING in a remote close (remoteRequestsClose ran first).
// Refuse only when CLOSING is unreachable (e.g. already CLOSED).
if (!ensureState(SocketState.CLOSING)) { return; }

Comment thread
borrrden marked this conversation as resolved.
// We've told Core to leave the connection to us, so it might pass us the HTTP status
// If it does, we need to convert it to a WS status for the other side.
Expand Down Expand Up @@ -566,6 +594,13 @@ private boolean assertState(@NonNull SocketState... expectedStates) {
synchronized (getLock()) { return state.assertState(expectedStates); }
}

// Idempotent transition to {@code target}: succeeds if we are already there
// or can legally transition there. Returned under getLock() so the check
// and the transition are atomic.
private boolean ensureState(@NonNull SocketState target) {
synchronized (getLock()) { return state.enterState(target); }
}

@Nullable
private byte[] encodeHeaders(@Nullable Map<String, Object> headers) {
try (FLEncoder enc = FLEncoder.getManagedEncoder()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,22 @@ public final boolean assertState(@NonNull T... expected) {
return false;
}

/**
* Idempotent transition. Returns true if the state machine is already in
* {@code target}, or if it can legally transition there. Returns false if
* no such transition is legal from the current state (e.g. {@code target}
* is unreachable from a terminal state). Unlike {@link #setState(Enum)},
* a no-op "already in target" outcome is silent — no debug exception
* trace is logged.
Comment thread
pasin marked this conversation as resolved.
*
* @param target the desired state.
* @return true if the current state ended up equal to {@code target}.
*/
public boolean enterState(@NonNull T target) {
if (state == target) { return true; }
return setState(target);
}

/**
* Set the new state.
* If it is legal to transition to the new state, from the current state, do so,
Expand Down