Module: Lich::InternalAPI::ActiveSessions::Lifecycle Private

Defined in:
documented/internal_api/active_sessions/lifecycle.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

Process-local lifecycle coordinator for active sessions registration.

This module adapts Lich runtime state into active-sessions payloads. It intentionally keeps only a small amount of mutable process-local state: identifying metadata, connection state, detachable listener details, and a heartbeat thread handle.

ActiveSessions API contract:

  • A session record being present in the registry means the Lich process is still known to the active-sessions service. Presence is not a promise that the game connection is still usable.
  • The connected field is the authoritative connection signal exposed to API consumers. Shutdown code must set it to false when the game connection has ended, before slower teardown work such as script before_dying hooks, state persistence, socket closeout, and database closeout runs.
  • Lifecycle stop unregisters the process from ActiveSessions. That removal means the process is no longer reporting as an active session; it should not be used merely to mean "the game connection dropped."
  • For detachable sessions, the public connected value is true only when both the game connection and detachable listener connection are active. A disconnected game session stays disconnected even if the detachable listener was last reported as connected.

Constant Summary collapse

HEARTBEAT_INTERVAL_SECONDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Default heartbeat cadence for refreshing the current process entry and detecting service-owner failover quickly enough for multi-session use.

Returns:

  • (Integer)
2

Class Method Summary collapse

Class Method Details

.clear_listenervoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Clears detachable listener metadata for the current session.

This is used when detachable listener infrastructure is torn down and the public snapshot should stop reporting a listener endpoint.



224
225
226
227
228
229
230
231
232
233
# File 'documented/internal_api/active_sessions/lifecycle.rb', line 224

def self.clear_listener
  return unless started?

  @mutex.synchronize do
    @listener_host = nil
    @listener_port = nil
    @listener_connected = false
  end
  upsert_current_session
end

.current_payloadHash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the current process session payload.

Returns:

  • (Hash)

    normalized payload suitable for registry upsert



274
275
276
# File 'documented/internal_api/active_sessions/lifecycle.rb', line 274

def self.current_payload
  @mutex.synchronize { build_current_payload }
end

.resolve_role(argv:, detachable_client_port:) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Resolves the logical runtime role for active sessions reporting.

Parameters:

  • argv (Array<String>)
  • detachable_client_port (Integer, nil)

Returns:



88
89
90
91
92
93
# File 'documented/internal_api/active_sessions/lifecycle.rb', line 88

def self.resolve_role(argv:, detachable_client_port:)
  return 'headless' if argv.include?('--without-frontend')
  return 'detachable' unless detachable_client_port.nil?

  'session'
end

.resolve_session_name(argv:, account_character: nil) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Resolves the reporting session name from runtime context.

Parameters:

  • argv (Array<String>)
  • account_character (String, nil) (defaults to: nil)

Returns:



71
72
73
74
75
76
77
78
79
80
81
# File 'documented/internal_api/active_sessions/lifecycle.rb', line 71

def self.resolve_session_name(argv:, account_character: nil)
  if ( = argv.index('--login')) && argv[ + 1]
    argv[ + 1].capitalize
  elsif  && !.to_s.empty?
    
  elsif defined?(XMLData) && XMLData.respond_to?(:name) && !XMLData.name.to_s.empty?
    XMLData.name
  else
    "pid-#{Process.pid}"
  end
end

.start(session_name:, role:, heartbeat_interval: HEARTBEAT_INTERVAL_SECONDS) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Starts lifecycle registration and periodic heartbeats.

Parameters:

  • session_name (String)
  • role (String)
  • heartbeat_interval (Integer) (defaults to: HEARTBEAT_INTERVAL_SECONDS)

Returns:

  • (Boolean)

    true when lifecycle tracking started



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'documented/internal_api/active_sessions/lifecycle.rb', line 101

def self.start(session_name:, role:, heartbeat_interval: HEARTBEAT_INTERVAL_SECONDS)
  feature_enabled = ActiveSessions.enabled?
  return false unless feature_enabled

  # Bootstrap once during lifecycle startup so the admitted-only
  # heartbeat/update path has a running service to talk to.
  ActiveSessions.ensure_service!

  thread = nil
  @mutex.synchronize do
    return false if @started

    @session_name = session_name
    @role = role
    @started_at = Time.now.to_i
    @connected = true
    @feature_enabled = feature_enabled
    @running = true
    @started = true
    @lifecycle_generation += 1
  end

  thread = Thread.new do
    loop do
      sleep heartbeat_interval
      break unless running?

      begin
        upsert_current_session
      rescue StandardError => e
        Lich.log("warning: ActiveSessions heartbeat tick failed (continuing): #{e.class}: #{e.message}\n\t#{e.backtrace&.first(3)&.join("\n\t")}") if Lich.respond_to?(:log)
      end
    end
  end

  @mutex.synchronize { @heartbeat_thread = thread if @started }

  upsert_current_session
  true
rescue StandardError => e
  @mutex.synchronize do
    @running = false
    @started = false
    @heartbeat_thread = nil
    @session_name = nil
    @role = nil
    @listener_host = nil
    @listener_port = nil
    @listener_connected = false
    @connected = true
    @started_at = nil
    @feature_enabled = false
  end
  thread.kill if thread.respond_to?(:alive?) && thread.alive?
  Lich.log("warning: ActiveSessions lifecycle start failed: #{e.class}: #{e.message}") if Lich.respond_to?(:log)
  false
end

.stopBoolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Stops lifecycle registration and removes the current process session.

Returns:

  • (Boolean)

    true when a running lifecycle was stopped



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'documented/internal_api/active_sessions/lifecycle.rb', line 162

def self.stop
  thread = nil
  lifecycle_active = false
  @mutex.synchronize do
    lifecycle_active = @started || !@heartbeat_thread.nil? || @running
    return false unless lifecycle_active

    @running = false
    @started = false
    @lifecycle_generation += 1
    thread = @heartbeat_thread
    @heartbeat_thread = nil
  end

  thread&.join(0.5)
  thread&.kill if thread&.alive?
  if feature_enabled?
    @registration_mutex.synchronize do
      ActiveSessions.send(:unregister_session_admitted, pid: Process.pid)
      ActiveSessions.cleanup_discovery_if_last_session!
    end
  end

  @mutex.synchronize do
    @session_name = nil
    @role = nil
    @listener_host = nil
    @listener_port = nil
    @listener_connected = false
    @connected = true
    @started_at = nil
    @feature_enabled = false
  end
  true
rescue StandardError => e
  Lich.log("warning: ActiveSessions lifecycle stop failed: #{e.class}: #{e.message}") if Lich.respond_to?(:log)
  false
end

.update_connected(connected) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Updates the current process connection state without unregistering the session from the active-sessions registry.

This method exists because MahtraDR's shutdown testing demonstrated that immediate unregister is too blunt an API signal: it hides a still-running Lich process from ActiveSessions while scripts, saves, socket closeout, and database closeout may still be executing. The narrower contract is to keep the session present while publishing connected: false.

API contract for callers:

  • update_connected(false) means the game/session connection is no longer available for normal use, but the Lich process may still be performing shutdown work.
  • update_connected(true) may restore the connection state for a started lifecycle without changing identity, uptime, listener metadata, or registration ownership.
  • The method is a no-op before start; it must not create a registry record or bootstrap ActiveSessions on its own.
  • The method updates the existing registry record through the same admitted lifecycle path used by heartbeats and listener updates; it must not unregister the session.
  • The published connected value is combined with detachable listener state, so a detachable session reports connected only when both the game connection and listener connection are active.

Parameters:

  • connected (Boolean)


264
265
266
267
268
269
# File 'documented/internal_api/active_sessions/lifecycle.rb', line 264

def self.update_connected(connected)
  return unless started?

  @mutex.synchronize { @connected = !!connected }
  upsert_current_session
end

.update_listener(host:, port:, connected:) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Updates detachable listener metadata for the current session.

Parameters:

  • host (String)
  • port (Integer)
  • connected (Boolean)


207
208
209
210
211
212
213
214
215
216
# File 'documented/internal_api/active_sessions/lifecycle.rb', line 207

def self.update_listener(host:, port:, connected:)
  return unless started?

  @mutex.synchronize do
    @listener_host = host
    @listener_port = port
    @listener_connected = connected
  end
  upsert_current_session
end