Skip to content

Add package-level Logon, Logout, and IsLoggedOn admin functions for runtime session control #753

@Mellywins

Description

@Mellywins

Summary

Propose adding three new package-level functions to enable programmatic
runtime control of individual FIX sessions:

  • Logon(sessionID SessionID) error
  • Logout(sessionID SessionID, reason string) error
  • IsLoggedOn(sessionID SessionID) (bool, error)

These mirror the existing pattern established by ResetSession,
SetNextTargetMsgSeqNum, SetNextSenderMsgSeqNum, etc. in registry.go.

Motivation

Operational tooling for FIX gateways commonly needs to control individual
sessions at runtime without bouncing the entire process:

  1. Force a clean logout (FIX 35=5) of a single session while preserving
    sequence numbers — e.g. when a downstream system needs the session
    paused for maintenance, when seqnum drift requires a coordinated
    reset-and-relogon cycle with the counterparty, or when the operator
    wants to disconnect a misbehaving counterparty cleanly rather than via
    TCP drop.
  2. Force a logon attempt on demand — e.g. after seqnum reset or after
    a transient downstream issue is resolved.
  3. Query logon state for status/health endpoints without requiring the
    application to mirror state via OnLogon/OnLogout callbacks.

Today the only exported per-session lifecycle primitive is ResetSession,
which has the side effect of resetting sequence numbers. This is too
destructive for the common operational workflow of "logout, fix something,
log back on, resume from the same seqnums."

The session type itself owns the relevant state (session.IsLoggedOn() on
the embedded stateMachine, session.sendLogon, session.initiateLogout)
but is unexported and unreachable from outside the package. There is
already a precedent for routing operations through the existing
session.admin channel (see connect, stopReq, waitForInSessionReq in
session.go), and this proposal follows that exact pattern.

Proposed API

// Logon requests that the session matching sessionID send a Logon (35=A)
// message if it is not already logged on. For acceptor-mode sessions, this
// re-enables the session for incoming Logon. No-op if already logged on.
// Returns errUnknownSession if no session is registered for sessionID.
func Logon(sessionID SessionID) error

// Logout requests that the session matching sessionID send a Logout (35=5)
// message and disconnect cleanly. Sequence numbers are NOT reset (use
// ResetSession for that). The reason argument is propagated as tag 58
// (Text) on the Logout message. No-op if not currently logged on.
// Returns errUnknownSession if no session is registered for sessionID.
func Logout(sessionID SessionID, reason string) error

// IsLoggedOn returns whether the session matching sessionID is currently
// in the LoggedOn state. Returns errUnknownSession if no session is
// registered for sessionID.
func IsLoggedOn(sessionID SessionID) (bool, error)

Implementation sketch

In registry.go, three new exported funcs that resolve the session via
the existing private lookupSession helper and dispatch a command on
session.admin (mirroring how stop() and connect() already work):

type logonReq  struct{ rep chan error }
type logoutReq struct{ reason string; rep chan error }

func Logon(sessionID SessionID) error {
    s, ok := lookupSession(sessionID)
    if !ok { return errUnknownSession }
    rep := make(chan error, 1)
    s.admin <- logonReq{rep: rep}
    return <-rep
}

func Logout(sessionID SessionID, reason string) error {
    s, ok := lookupSession(sessionID)
    if !ok { return errUnknownSession }
    rep := make(chan error, 1)
    s.admin <- logoutReq{reason: reason, rep: rep}
    return <-rep
}

func IsLoggedOn(sessionID SessionID) (bool, error) {
    s, ok := lookupSession(sessionID)
    if !ok { return false, errUnknownSession }
    return s.IsLoggedOn(), nil
}

In session.go, two new cases in the onAdmin switch that delegate to
the existing private sendLogon / initiateLogout helpers:

case logonReq:
    if s.IsLoggedOn() { msg.rep <- nil; return }
    msg.rep <- s.sendLogon()
case logoutReq:
    if !s.IsLoggedOn() { msg.rep <- nil; return }
    msg.rep <- s.initiateLogout(msg.reason)

Why the admin channel

The session.admin channel is the existing serialization point for
external commands into the session goroutine (currently used by connect
and stop). Routing Logon/Logout through it preserves the
single-goroutine-per-session invariant and avoids new locking — the same
pattern that makes the rest of registry.go safe.

Backward compatibility

Purely additive — three new exported symbols and two new cases in an
internal switch. No existing API or behavior is changed.

Happy to open the PR if this direction is acceptable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions