Class AgentConnection

java.lang.Object
com.ultikits.ultitools.utils.AgentConnection

final class AgentConnection extends Object
The reconnect state machine of the UltiCloud v2 agent connection (as of 6.3.1).

It is the single owner of every connect, retry and terminal decision for the /v1/agent socket: nothing else opens a socket, schedules a retry or decides that the connection is over. (v1 had four owners deciding whether to keep trying.)

Transitions
FromEventTo
IDLE, STOPPED, terminalstart() with a credentialCONNECTING (fresh cycle)
CONNECTINGupgrade acceptedOPEN (not yet established)
OPENmarkEstablished() (a valid welcome)OPEN, established: counters reset, heartbeat started
CONNECTINGrefused 401CREDENTIAL_REJECTED
CONNECTINGrefused 426UPGRADE_REQUIRED
CONNECTINGrefused 402PLAN_LIMITED (fixed plan-limit interval)
CONNECTINGrefused 429BACKOFF (long, at least Retry-After)
CONNECTINGrefused with another 4xxBACKOFF (long)
CONNECTINGrefused with 5xx, network error, close, handshake deadlineBACKOFF (normal)
OPEN, not establishedhandshake deadline (no welcome), abandonAttempt(String) BACKOFF (normal)
OPENstopForServerIdMismatch()SERVER_ID_MISMATCH
OPEN, CONNECTINGclose 4401 / 4426CREDENTIAL_REJECTED / UPGRADE_REQUIRED
OPEN, CONNECTINGclose 4402PLAN_LIMITED (one probe after the attempt-0 delay)
OPEN, CONNECTINGclose 4400, 4429BACKOFF (long)
OPEN, CONNECTINGclose 4000BACKOFF (long), or SUPERSEDED on the third in a row
OPENany other close, heartbeat timeoutBACKOFF (normal)
BACKOFF, PLAN_LIMITEDretry due, or reconnectNow()CONNECTING
SUPERSEDEDreconnectNow()CONNECTING (counters reset)
anystop()STOPPED
What the owner deletes or prints on a terminal state is the owner's decision (the cloud session); this class only reports it, once, through the AgentConnection.Listener.

Why an attempt id. v1 reconnected by reusing one client object, so a reconnect carried the stale headers (and bearer) of the first connect, and a late callback of an old socket could change the state of a new one. Here every attempt builds a new socket with the bearer read at that moment, and every socket's events are bound to the id (generation) the attempt had when it started; the id only ever increases (an attempt's end, a retry, stop() and a terminal state each move it on), so an event whose id is not the current one is ignored.

Threads and locks. Socket callbacks arrive on Java-WebSocket's threads (or synchronously inside a forced close); each one only posts a task onto the single-thread agent scheduler. All state changes run under this instance's monitor. The monitor is never held while calling out: listener callbacks, socket opens, closes and sends, the credential supplier and the log sink all run after it is released, so a listener may call stop(). In the session's lock order this monitor sits beside CredentialStore.LOCK and the state-file write lock, below the session monitor; nothing here blocks or waits (no connectBlocking, no Future#get).

Every contract number lives in the one contract section below.

  • Field Details

    • HTTP_INVALID_CREDENTIAL

      static final int HTTP_INVALID_CREDENTIAL
      Upgrade refused: the credential is invalid. Stop and delete the credential.
      See Also:
    • HTTP_PLAN_LIMIT

      static final int HTTP_PLAN_LIMIT
      Upgrade refused: the server is suspended for exceeding its plan. Keep the credential.
      See Also:
    • HTTP_UPGRADE_REQUIRED

      static final int HTTP_UPGRADE_REQUIRED
      Upgrade refused: protocol or framework too old. Stop and keep the credential.
      See Also:
    • HTTP_TOO_MANY_REQUESTS

      static final int HTTP_TOO_MANY_REQUESTS
      Upgrade refused: rate limited. Long backoff, at least Retry-After.
      See Also:
    • HTTP_CLIENT_ERROR_MIN

      static final int HTTP_CLIENT_ERROR_MIN
      The first 4xx status; every 4xx not named above backs off long.
      See Also:
    • HTTP_CLIENT_ERROR_MAX

      static final int HTTP_CLIENT_ERROR_MAX
      The last 4xx status.
      See Also:
    • CLOSE_PLAN_LIMIT

      static final int CLOSE_PLAN_LIMIT
      Closed: the server's plan limit applies; same handling as HTTP 402.
      See Also:
    • CLOSE_SUPERSEDED

      static final int CLOSE_SUPERSEDED
      Closed: another connection with the same credential replaced this one.
      See Also:
    • CLOSE_INVALID_HELLO

      static final int CLOSE_INVALID_HELLO
      Closed: an invalid hello, or a frame before welcome.
      See Also:
    • CLOSE_REVOKED

      static final int CLOSE_REVOKED
      Closed: the credential was revoked.
      See Also:
    • CLOSE_NO_HELLO

      static final int CLOSE_NO_HELLO
      Closed: no hello within the gateway's deadline.
      See Also:
    • CLOSE_PROTOCOL_MISMATCH

      static final int CLOSE_PROTOCOL_MISMATCH
      Closed: the hello's protocol does not match the upgrade header.
      See Also:
    • CLOSE_ABUSE_RATE

      static final int CLOSE_ABUSE_RATE
      Closed: abuse rate (including too many invalid frames).
      See Also:
    • CLOSE_NORMAL

      static final int CLOSE_NORMAL
      The normal close code this side uses for its own closes.
      See Also:
    • CLOSE_NEVER_CONNECTED

      static final int CLOSE_NEVER_CONNECTED
      Java-WebSocket's code for a socket that never connected.
      See Also:
    • SUPERSEDED_STOP_AFTER

      static final int SUPERSEDED_STOP_AFTER
      Consecutive 4000 closes, with no long-lived connection between them, that end retrying.
      See Also:
    • PLAN_LIMIT_RETRY_MS

      static final long PLAN_LIMIT_RETRY_MS
      Fixed plan-limit retry interval: 15 minutes, no jitter, no growth.
      See Also:
    • RETRY_AFTER_MAX_SECONDS

      static final long RETRY_AFTER_MAX_SECONDS
      Largest Retry-After honoured, in seconds.
      See Also:
    • SUPERSEDED_RESET_MS

      static final long SUPERSEDED_RESET_MS
      A connection open at least this long resets the consecutive-4000 count.
      See Also:
    • HANDSHAKE_DEADLINE_MS

      static final long HANDSHAKE_DEADLINE_MS
      Deadline for an attempt to be established: upgraded and answered with a valid welcome.
      See Also:
    • CONNECT_TIMEOUT_MS

      static final int CONNECT_TIMEOUT_MS
      TCP connect timeout handed to the socket's constructor.
      See Also:
    • HEARTBEAT_INTERVAL_MS

      static final long HEARTBEAT_INTERVAL_MS
      Heartbeat: the text frame HEARTBEAT_PING is sent this often once established.
      See Also:
    • HEARTBEAT_TIMEOUT_MS

      static final long HEARTBEAT_TIMEOUT_MS
      Heartbeat: without a HEARTBEAT_PONG for this long the connection is closed.
      See Also:
    • HEARTBEAT_PING

      static final String HEARTBEAT_PING
      The heartbeat request text frame.
      See Also:
    • HEARTBEAT_PONG

      static final String HEARTBEAT_PONG
      The heartbeat reply text frame.
      See Also:
    • AUTHORIZATION_HEADER

      static final String AUTHORIZATION_HEADER
      Upgrade header carrying the credential.
      See Also:
    • PROTOCOL_HEADER

      static final String PROTOCOL_HEADER
      Upgrade header carrying the protocol major version.
      See Also:
    • FRAMEWORK_HEADER

      static final String FRAMEWORK_HEADER
      Upgrade header carrying the running framework version.
      See Also:
    • MIN_FRAMEWORK_HEADER

      static final String MIN_FRAMEWORK_HEADER
      426 response header carrying the minimum framework version.
      See Also:
    • PROTOCOL_VERSION

      static final String PROTOCOL_VERSION
      The protocol version this client speaks.
      See Also:
    • endpoint

      private final URI endpoint
    • bearerSupplier

      private final Supplier<String> bearerSupplier
    • frameworkVersion

      private final String frameworkVersion
    • scheduler

      private final ScheduledExecutorService scheduler
    • listener

      private final AgentConnection.Listener listener
    • sockets

      private final AgentConnection.SocketFactory sockets
    • policy

      private final ReconnectPolicy policy
    • clock

      private final LongSupplier clock
    • timing

      private final AgentConnection.Timing timing
    • log

      private final BiConsumer<Level,String> log
    • state

      private AgentConnection.State state
    • generation

      private long generation
      Identifies the live attempt or pending retry; only ever increases.
    • handle

      private AgentConnection.Handle handle
    • attemptsSinceEstablished

      private int attemptsSinceEstablished
    • currentAttemptNumber

      private int currentAttemptNumber
    • normalFailures

      private int normalFailures
    • longFailures

      private int longFailures
    • consecutiveSuperseded

      private int consecutiveSuperseded
    • openedAtMillis

      private long openedAtMillis
    • established

      private boolean established
      Whether the open attempt was answered with a valid welcome (markEstablished()).
    • retryFuture

      private ScheduledFuture<?> retryFuture
    • deadlineFuture

      private ScheduledFuture<?> deadlineFuture
    • heartbeatFuture

      private ScheduledFuture<?> heartbeatFuture
    • lastPongMillis

      private long lastPongMillis
    • retryAtMillis

      private long retryAtMillis
    • planEpisode

      private boolean planEpisode
    • planNoticeSent

      private boolean planNoticeSent
  • Constructor Details

  • Method Details

    • forRefusal

      static AgentConnection.Disposition forRefusal(int httpStatus)
      Classifies a refused upgrade.
      Parameters:
      httpStatus - the HTTP status of the refused upgrade
      Returns:
      its disposition; another 4xx retries long, a 5xx (and anything else) normally
    • forClose

      static AgentConnection.Disposition forClose(int code)
      Classifies a close of an upgraded (or never-upgraded) socket.
      Parameters:
      code - the close code
      Returns:
      its disposition; every code not named here (1000, 1001, 1006, 1009, 1011, 4408, unknown) retries normally
    • realSockets

      static AgentConnection.SocketFactory realSockets(int connectTimeoutMs, BiConsumer<Level,String> log)
      The production socket factory: a fresh AgentSocket per attempt, connected with the non-blocking connect() (never connectBlocking).
    • buildHeaders

      Map<String,String> buildHeaders(String bearer)
      The upgrade headers: exactly the credential, the protocol version and the framework version.
      Parameters:
      bearer - the credential
      Returns:
      the headers, in a fixed order
    • start

      void start()
      Starts a fresh cycle from IDLE, STOPPED or a terminal state, with every counter reset. A no-op while connecting, open, backing off or plan-limited. Without a credential the connection goes to STOPPED.
    • stop

      void stop()
      Stops: cancels the pending retry and closes the socket with 1000. Events from older sockets are ignored afterwards.
    • reconnectNow

      Retries at once from BACKOFF, PLAN_LIMITED or SUPERSEDED, cancelling the pending retry (from SUPERSEDED with every counter reset; from PLAN_LIMITED the plan-limit episode continues, so a renewed 402 sends no second notice). Used by /ulticloud reconnect.
      Returns:
      STARTED, or why nothing was started
    • reconnectRefusal

      private static AgentConnection.ReconnectOutcome reconnectRefusal(AgentConnection.State s)
    • markEstablished

      void markEstablished()
      Marks the open connection as established, called by the protocol layer when a valid welcome answers the hello (D-12, contract SEMANTICS 10): resets the normal and long backoff counters and the attempt number, cancels the handshake deadline and starts the text heartbeat. A ping before welcome would be closed with 4400, so nothing here runs at the upgrade itself. A no-op unless OPEN; never calls out.
    • abandonAttempt

      void abandonAttempt(String reason)
      Ends the open attempt because the protocol layer refused what the gateway sent (an invalid welcome, S-51): the socket is closed with 1000 and the normal backoff follows. A no-op unless OPEN.
      Parameters:
      reason - the reason for the retry line, never carrying payload or credential text
    • reconnectForNewHello

      void reconnectForNewHello(String reason)
      Closes the open attempt with 1000 so the next attempt sends a new hello (22-22 fix: a reload changed the capability set, which is negotiated only at the handshake); the normal backoff follows, which is short after an established connection. Runs on the scheduler, like every socket event, so the close listeners run on the agent thread. A no-op unless the attempt open now is still the open one when it runs.
      Parameters:
      reason - the reason for the retry line, never carrying payload or credential text
    • stopForServerIdMismatch

      void stopForServerIdMismatch()
      Stops for good because the welcome named another server id than the linked one (contract answer 21, SEMANTICS 16): the socket is closed with 1000, no retry is scheduled and the owner is told once through AgentConnection.Listener.onServerIdMismatch(); the credential is kept. reconnectNow() does not leave the resulting state. A no-op unless OPEN.
    • frameworkVersion

      String frameworkVersion()
      Returns:
      the running framework version this connection sends in its upgrade headers
    • isEstablished

      boolean isEstablished()
      Returns:
      whether the open connection was answered with a valid welcome
    • send

      boolean send(String text)
      Sends a text frame.
      Parameters:
      text - the frame
      Returns:
      whether it was handed to an open socket; false unless OPEN
    • state

      Returns:
      the current state
    • attempt

      int attempt()
      Returns:
      the 1-based number of the current or last attempt since the last established connection
    • nextRetryAtMillis

      long nextRetryAtMillis()
      Returns:
      when the pending retry runs, from the injected clock; 0 when none is pending
    • isStartable

      private static boolean isStartable(AgentConnection.State s)
    • resetCycle

      private void resetCycle()
    • silent

      private boolean silent()
    • beginAttempt

      private void beginAttempt(String bearer, AgentConnection.Effects out)
      Under the monitor: enters CONNECTING and queues the socket open.
    • openSocket

      private void openSocket(long id, Map<String,String> headers)
      Outside the monitor: opens the attempt's socket and keeps it if the attempt is still live.
    • handleOpen

      private void handleOpen(long id)
    • handleText

      private void handleText(long id, String text)
    • handleRefused

      private void handleRefused(long id, int status, UpgradeRejection rejection)
    • handleClosed

      private void handleClosed(long id, int code)
    • onAttemptFailed

      private void onAttemptFailed(long id, String reason)
    • planLimitRefusal

      private void planLimitRefusal(UpgradeRejection rejection, String reason, AgentConnection.Effects out)
      Under the monitor: a 402 refusal.
    • planLimitClose

      private void planLimitClose(String reason, AgentConnection.Effects out)
      Under the monitor: close 4402. The close frame cannot carry the reason or the manage link, so one probe follows after the normal attempt-0 delay; its 402 sends the notice.
    • endPlanEpisode

      private void endPlanEpisode()
      Under the monitor: a failure other than the plan limit ends a plan-limit episode.
    • retryNormal

      private void retryNormal(String reason, AgentConnection.Effects out)
    • retryLong

      private void retryLong(String reason, long retryAfterSeconds, AgentConnection.Effects out)
      Under the monitor: a long-class failure; a 429 waits at least its Retry-After.
    • superseded

      private void superseded(boolean afterLongLivedConnection, AgentConnection.Effects out)
      Under the monitor: close 4000. Three in a row with no connection lasting supersededResetMs in between stop retrying (credential kept); the earlier ones back off long with a warning instead of the retry line.
    • scheduleRetry

      private boolean scheduleRetry(AgentConnection.State next, long delayMs, String reason, Level level, AgentConnection.Effects out)
      Under the monitor: ends the attempt and schedules the next one; logs the retry line unless reason is null (the caller logs its own line).
      Returns:
      whether the retry was scheduled
    • runRetry

      private void runRetry(long token)
    • terminal

      private void terminal(AgentConnection.State terminalState, Runnable callback, AgentConnection.Effects out)
      Under the monitor: enters a terminal state.
    • handshakeDeadline

      private void handshakeDeadline(long id)
      On the scheduler: the attempt's handshake deadline, covering the upgrade and the welcome.
    • scheduleHeartbeat

      private void scheduleHeartbeat(long id)
      Under the monitor: the next heartbeat tick of the established connection id.
    • heartbeatTick

      private void heartbeatTick(long id)
      On the scheduler: sends the text frame ping, or closes and retries when no pong arrived within the heartbeat timeout (contract answer 2; the library's ping control frames are disabled).
    • cancelDeadline

      private void cancelDeadline()
    • cancelTimers

      private void cancelTimers()
    • cancelRetry

      private void cancelRetry()
    • moveTo

      private void moveTo(AgentConnection.State next, AgentConnection.Effects out)
    • readBearer

      private String readBearer()
    • post

      private void post(Runnable task)
    • bump

      private static int bump(int counter)
    • seconds

      private static long seconds(long millis)