Class: MCPClient::ServerStdio::ChildSession

Inherits:
Object
  • Object
show all
Defined in:
lib/mcp_client/server_stdio/child_session.rb

Overview

The record of one server child process: when the open subscriptions were handed to it, and when its handles were torn down. One record per process, written only by that process's own lifecycle, so nothing another thread does to another process can change what this one says.

It exists for the crash-loop bound. That bound used to live in flags and a timestamp on the transport, which two restarts (or a restart and a host request that re-established the process first) raced over: whichever finished last decided what the other process's uptime had been, and a server that exited on sight could be respawned for ever. The question a restart actually has to answer is about one particular process — "did the last process we gave these subscriptions to die straight after we gave them to it?" — and both facts it needs are recorded here, on that process, at the two moments they happen.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize ⇒ ChildSession

Returns a new instance of ChildSession.



27
28
29
30
31
# File 'lib/mcp_client/server_stdio/child_session.rb', line 27

def initialize
  @carried_at = nil
  @ended_at = nil
  @exited_unexpectedly = false
end

Instance Attribute Details

#carried_at ⇒ Float? (readonly)

Returns monotonic time the open subscriptions were re-sent to this process, nil if it was never asked to carry any.

Returns:

  • (Float, nil) —

    monotonic time the open subscriptions were re-sent to this process, nil if it was never asked to carry any



22
23
24
# File 'lib/mcp_client/server_stdio/child_session.rb', line 22

def carried_at
  @carried_at
end

#ended_at ⇒ Float? (readonly)

Returns monotonic time this process's handles were torn down, nil while it is still the live session.

Returns:

  • (Float, nil) —

    monotonic time this process's handles were torn down, nil while it is still the live session



25
26
27
# File 'lib/mcp_client/server_stdio/child_session.rb', line 25

def ended_at
  @ended_at
end

Class Method Details

.now ⇒ Float

Returns monotonic seconds.

Returns:

  • (Float) —

    monotonic seconds



40
41
42
# File 'lib/mcp_client/server_stdio/child_session.rb', line 40

def self.now
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
end

Instance Method Details

#carrying_subscriptions ⇒ void

This method returns an undefined value.

This process has been handed the subscriptions a previous process left open. Stamped before they go out, not after: the process can exit while they are still being written, and an exit under the re-send is the clearest crash loop there is.



49
50
51
# File 'lib/mcp_client/server_stdio/child_session.rb', line 49

def carrying_subscriptions
  @carried_at = ChildSession.now
end

#died_carrying_subscriptions?(min_uptime) ⇒ Boolean

Whether this process died too soon after being given the subscriptions to be given them again — the crash loop the restart bound exists to stop. A process that was never given any is not part of that loop, and one that is still alive has not ended anything.

Neither is a process the client itself shut down. A host that closes the transport and reconnects — as a cleanup/request cycle does, and as re-authenticating or re-configuring a server does — tears the process down whenever it likes, and reading that as the server crashing closed the very subscriptions the reconnect exists to carry across. Only an exit the reader actually watched happen counts.

The interval is measured from the moment it received them, so a server that is slow to start is credited with none of its own handshake, and a process that was already gone when they were sent to it (its exit stamped before the re-send) counts as having survived no time at all.

Parameters:

Returns:

  • (Boolean)


91
92
93
94
95
# File 'lib/mcp_client/server_stdio/child_session.rb', line 91

def died_carrying_subscriptions?(min_uptime)
  return false unless @exited_unexpectedly && @carried_at && @ended_at

  (@ended_at - @carried_at) < min_uptime
end

#ended ⇒ void

This method returns an undefined value.

This process is gone (its stdio handles have been torn down). First stamp wins: a host cleanup racing the reader's own teardown must not move the moment the process ended.



57
58
59
60
61
# File 'lib/mcp_client/server_stdio/child_session.rb', line 57

def ended
  return if @ended_at

  @ended_at = ChildSession.now
end

#exited_unexpectedly ⇒ void

This method returns an undefined value.

This process exited on its own — the reader saw EOF on a stdin the client had not closed (see MCPClient::ServerStdio#handle_server_exit). Recorded separately from #ended, which every teardown stamps, because only an exit is a crash.



68
69
70
# File 'lib/mcp_client/server_stdio/child_session.rb', line 68

def exited_unexpectedly
  @exited_unexpectedly = true
end

#exited_unexpectedly? ⇒ Boolean

Returns whether this process went on its own rather than being torn down by the client.

Returns:

  • (Boolean) —

    whether this process went on its own rather than being torn down by the client



35
36
37
# File 'lib/mcp_client/server_stdio/child_session.rb', line 35

def exited_unexpectedly?
  @exited_unexpectedly
end