import { ITelemetryLogger } from "@fluidframework/common-definitions" ; import { IConnectionDetails } from "@fluidframework/container-definitions" ; import { ProtocolOpHandler } from "@fluidframework/protocol-base" ; import { ConnectionMode , ISequencedClient } from "@fluidframework/protocol-definitions" ; import { PerformanceEvent } from "@fluidframework/telemetry-utils" ; import { assert , Timer } from "@fluidframework/common-utils" ; import { ConnectionState } from "./container" ; export interface IConnectionStateHandler { protocolHandler : ( ) => ProtocolOpHandler | undefined ; logConnectionStateChangeTelemetry : ( value : ConnectionState , oldState : ConnectionState , reason ? : string | undefined ) => void ; shouldClientJoinWrite : ( ) => boolean ; maxClientLeaveWaitTime : number | undefined ; logConnectionIssue : ( eventName : string ) => void ; connectionStateChanged : ( ) => void ; } export interface ILocalSequencedClient extends ISequencedClient { shouldHaveLeft ? : boolean ; } const JoinOpTimer = 45000 ; export class ConnectionStateHandler { private _connectionState = ConnectionState . Disconnected ; private _pendingClientId : string | undefined ; private _clientId : string | undefined ; private readonly prevClientLeftTimer : Timer ; private readonly joinOpTimer : Timer ; private waitEvent : PerformanceEvent | undefined ; public get connectionState ( ) : ConnectionState { return this . _connectionState ; } public get connected ( ) : boolean { return this . connectionState === ConnectionState . Connected ; } public get clientId ( ) : string | undefined { return this . _clientId ; } public get pendingClientId ( ) : string | undefined { return this . _pendingClientId ; } constructor ( private readonly handler , private readonly logger ) { this . prevClientLeftTimer = new Timer ( this . handler . maxClientLeaveWaitTime ?? 90000 , ( ) => { assert ( ! this . connected , 0x2ac ) ; this . applyForConnectedState ( "timeout" ) ; } ) ; this . joinOpTimer = new Timer ( JoinOpTimer , ( ) => { if ( this . connectionState === ConnectionState . Connecting ) { this . handler . logConnectionIssue ( "NoJoinOp" ) ; } } ) ; } private startJoinOpTimer ( ) { assert ( ! this . joinOpTimer . hasTimer , 0x234 ) ; this . joinOpTimer . start ( ) ; } private stopJoinOpTimer ( ) { assert ( this . joinOpTimer . hasTimer , 0x235 ) ; this . joinOpTimer . clear ( ) ; } public dispose ( ) { assert ( ! this . joinOpTimer . hasTimer , 0x2a5 ) ; this . prevClientLeftTimer . clear ( ) ; } public receivedAddMemberEvent ( clientId ) { if ( clientId === this . pendingClientId ) { if ( this . joinOpTimer . hasTimer ) { this . stopJoinOpTimer ( ) ; } else { this . handler . logConnectionIssue ( "ReceivedJoinOp" ) ; } if ( this . prevClientLeftTimer . hasTimer ) { this . waitEvent = PerformanceEvent . start ( this . logger , { eventName : "WaitBeforeClientLeave" , waitOnClientId : this . _clientId , hadOutstandingOps : this . handler . shouldClientJoinWrite ( ) } ) ; } this . applyForConnectedState ( "addMemberEvent" ) ; } } private applyForConnectedState ( source ) { const protocolHandler = this . handler . protocolHandler ( ) ; assert ( protocolHandler !== undefined , 0x236 ) ; if ( this . pendingClientId !== this . clientId && this . pendingClientId !== undefined && protocolHandler . quorum . getMember ( this . pendingClientId ) !== undefined && ! this . prevClientLeftTimer . hasTimer ) { this . waitEvent ?. end ( { source } ) ; this . setConnectionState ( ConnectionState . Connected ) ; } else { this . logger . sendTelemetryEvent ( { eventName : "connectedStateRejected" , category : source === "timeout" ? "error" : "generic" , source , pendingClientId : this . pendingClientId , clientId : this . clientId , hasTimer : this . prevClientLeftTimer . hasTimer , inQuorum : protocolHandler !== undefined && this . pendingClientId !== undefined && protocolHandler . quorum . getMember ( this . pendingClientId ) !== undefined } ) ; } } public receivedRemoveMemberEvent ( clientId ) { if ( this . clientId === clientId ) { this . prevClientLeftTimer . clear ( ) ; this . applyForConnectedState ( "removeMemberEvent" ) ; } } public receivedDisconnectEvent ( reason ) { if ( this . joinOpTimer . hasTimer ) { this . stopJoinOpTimer ( ) ; } this . setConnectionState ( ConnectionState . Disconnected , reason ) ; } public receivedConnectEvent ( connectionMode , details ) { const oldState = this . _connectionState ; this . _connectionState = ConnectionState . Connecting ; this . _pendingClientId = details . clientId ; this . handler . logConnectionStateChangeTelemetry ( ConnectionState . Connecting , oldState ) ; const protocolHandler = this . handler . protocolHandler ( ) ; if ( ( protocolHandler !== undefined && protocolHandler . quorum . getMember ( details . clientId ) !== undefined ) || connectionMode === "read" ) { assert ( ! this . prevClientLeftTimer . hasTimer , 0x2a6 ) ; this . setConnectionState ( ConnectionState . Connected ) ; } else if ( connectionMode === "write" ) { this . startJoinOpTimer ( ) ; } } private setConnectionState ( value , reason ) ; private setConnectionState ( value ) ; private setConnectionState ( value , reason ? ) { if ( this . connectionState === value ) { this . logger . sendErrorEvent ( { eventName : "setConnectionStateSame" , value } ) ; return ; } const oldState = this . _connectionState ; this . _connectionState = value ; const quorum = this . handler . protocolHandler ( ) ?. quorum ; let client ; if ( this . _clientId !== undefined ) { client = quorum ?. getMember ( this . _clientId ) ; } if ( value === ConnectionState . Connected ) { assert ( oldState === ConnectionState . Connecting , 0x1d8 ) ; if ( client !== undefined ) { client . shouldHaveLeft = true ; } this . _clientId = this . pendingClientId ; } else if ( value === ConnectionState . Disconnected ) { this . _pendingClientId = undefined ; if ( client !== undefined && this . handler . shouldClientJoinWrite ( ) && this . prevClientLeftTimer . hasTimer === false ) { this . prevClientLeftTimer . restart ( ) ; } else { this . logger . sendTelemetryEvent ( { eventName : "noWaitOnDisconnected" , inQuorum : client !== undefined , hasTimer : this . prevClientLeftTimer . hasTimer , shouldClientJoinWrite : this . handler . shouldClientJoinWrite ( ) } ) ; } } this . handler . logConnectionStateChangeTelemetry ( this . _connectionState , oldState , reason ) ; this . handler . connectionStateChanged ( ) ; } }