ruby_llm-mcp 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +144 -162
- data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +21 -4
- data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +20 -0
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +215 -0
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +413 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +41 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +56 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +56 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +90 -0
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +216 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +7 -1
- data/lib/ruby_llm/mcp/auth/browser/http_server.rb +0 -3
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +0 -2
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +100 -32
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +230 -57
- data/lib/ruby_llm/mcp/auth/discoverer.rb +157 -26
- data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +19 -2
- data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +3 -2
- data/lib/ruby_llm/mcp/auth/http_response_handler.rb +0 -2
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +31 -12
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +124 -9
- data/lib/ruby_llm/mcp/auth/session_manager.rb +0 -2
- data/lib/ruby_llm/mcp/auth/token_manager.rb +74 -3
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +72 -15
- data/lib/ruby_llm/mcp/auth.rb +19 -7
- data/lib/ruby_llm/mcp/client.rb +267 -39
- data/lib/ruby_llm/mcp/configuration.rb +161 -12
- data/lib/ruby_llm/mcp/elicitation.rb +261 -14
- data/lib/ruby_llm/mcp/errors.rb +18 -0
- data/lib/ruby_llm/mcp/extensions/apps/constants.rb +28 -0
- data/lib/ruby_llm/mcp/extensions/apps/resource_metadata.rb +24 -0
- data/lib/ruby_llm/mcp/extensions/apps/tool_metadata.rb +45 -0
- data/lib/ruby_llm/mcp/extensions/configuration.rb +72 -0
- data/lib/ruby_llm/mcp/extensions/constants.rb +16 -0
- data/lib/ruby_llm/mcp/extensions/registry.rb +85 -0
- data/lib/ruby_llm/mcp/handlers/approval_decision.rb +90 -0
- data/lib/ruby_llm/mcp/handlers/async_response.rb +181 -0
- data/lib/ruby_llm/mcp/handlers/concerns/approval_actions.rb +42 -0
- data/lib/ruby_llm/mcp/handlers/concerns/async_execution.rb +80 -0
- data/lib/ruby_llm/mcp/handlers/concerns/elicitation_actions.rb +42 -0
- data/lib/ruby_llm/mcp/handlers/concerns/error_handling.rb +29 -0
- data/lib/ruby_llm/mcp/handlers/concerns/guard_checks.rb +72 -0
- data/lib/ruby_llm/mcp/handlers/concerns/lifecycle.rb +84 -0
- data/lib/ruby_llm/mcp/handlers/concerns/logging.rb +19 -0
- data/lib/ruby_llm/mcp/handlers/concerns/model_filtering.rb +36 -0
- data/lib/ruby_llm/mcp/handlers/concerns/options.rb +83 -0
- data/lib/ruby_llm/mcp/handlers/concerns/registry_integration.rb +54 -0
- data/lib/ruby_llm/mcp/handlers/concerns/sampling_actions.rb +84 -0
- data/lib/ruby_llm/mcp/handlers/concerns/timeouts.rb +52 -0
- data/lib/ruby_llm/mcp/handlers/concerns/tool_filtering.rb +50 -0
- data/lib/ruby_llm/mcp/handlers/elicitation_handler.rb +58 -0
- data/lib/ruby_llm/mcp/handlers/elicitation_registry.rb +203 -0
- data/lib/ruby_llm/mcp/handlers/human_in_the_loop_handler.rb +93 -0
- data/lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb +271 -0
- data/lib/ruby_llm/mcp/handlers/promise.rb +192 -0
- data/lib/ruby_llm/mcp/handlers/sampling_handler.rb +64 -0
- data/lib/ruby_llm/mcp/handlers.rb +14 -0
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +94 -0
- data/lib/ruby_llm/mcp/native/client.rb +551 -0
- data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
- data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
- data/lib/ruby_llm/mcp/native/messages/notifications.rb +60 -0
- data/lib/ruby_llm/mcp/native/messages/requests.rb +267 -0
- data/lib/ruby_llm/mcp/native/messages/responses.rb +114 -0
- data/lib/ruby_llm/mcp/native/messages.rb +43 -0
- data/lib/ruby_llm/mcp/native/notification.rb +16 -0
- data/lib/ruby_llm/mcp/native/protocol.rb +79 -0
- data/lib/ruby_llm/mcp/native/response_handler.rb +220 -0
- data/lib/ruby_llm/mcp/native/task_registry.rb +62 -0
- data/lib/ruby_llm/mcp/native/transport.rb +88 -0
- data/lib/ruby_llm/mcp/native/transports/sse.rb +655 -0
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +367 -0
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +1024 -0
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/native/transports/support/rate_limiter.rb +49 -0
- data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
- data/lib/ruby_llm/mcp/native.rb +12 -0
- data/lib/ruby_llm/mcp/notification_handler.rb +43 -5
- data/lib/ruby_llm/mcp/prompt.rb +7 -7
- data/lib/ruby_llm/mcp/railtie.rb +8 -6
- data/lib/ruby_llm/mcp/resource.rb +17 -8
- data/lib/ruby_llm/mcp/resource_template.rb +8 -7
- data/lib/ruby_llm/mcp/result.rb +8 -4
- data/lib/ruby_llm/mcp/roots.rb +4 -4
- data/lib/ruby_llm/mcp/sample.rb +83 -13
- data/lib/ruby_llm/mcp/schema_validator.rb +33 -0
- data/lib/ruby_llm/mcp/server_capabilities.rb +41 -0
- data/lib/ruby_llm/mcp/task.rb +65 -0
- data/lib/ruby_llm/mcp/tool.rb +33 -27
- data/lib/ruby_llm/mcp/version.rb +1 -1
- data/lib/ruby_llm/mcp.rb +31 -7
- data/lib/tasks/smoke.rake +66 -0
- metadata +77 -36
- data/lib/ruby_llm/mcp/coordinator.rb +0 -304
- data/lib/ruby_llm/mcp/notifications/cancelled.rb +0 -32
- data/lib/ruby_llm/mcp/notifications/initialize.rb +0 -24
- data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +0 -26
- data/lib/ruby_llm/mcp/protocol.rb +0 -34
- data/lib/ruby_llm/mcp/requests/completion_prompt.rb +0 -50
- data/lib/ruby_llm/mcp/requests/completion_resource.rb +0 -50
- data/lib/ruby_llm/mcp/requests/initialization.rb +0 -34
- data/lib/ruby_llm/mcp/requests/logging_set_level.rb +0 -28
- data/lib/ruby_llm/mcp/requests/ping.rb +0 -24
- data/lib/ruby_llm/mcp/requests/prompt_call.rb +0 -32
- data/lib/ruby_llm/mcp/requests/prompt_list.rb +0 -31
- data/lib/ruby_llm/mcp/requests/resource_list.rb +0 -31
- data/lib/ruby_llm/mcp/requests/resource_read.rb +0 -30
- data/lib/ruby_llm/mcp/requests/resource_template_list.rb +0 -31
- data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +0 -30
- data/lib/ruby_llm/mcp/requests/shared/meta.rb +0 -32
- data/lib/ruby_llm/mcp/requests/shared/pagination.rb +0 -17
- data/lib/ruby_llm/mcp/requests/tool_call.rb +0 -35
- data/lib/ruby_llm/mcp/requests/tool_list.rb +0 -31
- data/lib/ruby_llm/mcp/response_handler.rb +0 -67
- data/lib/ruby_llm/mcp/responses/elicitation.rb +0 -33
- data/lib/ruby_llm/mcp/responses/error.rb +0 -33
- data/lib/ruby_llm/mcp/responses/ping.rb +0 -28
- data/lib/ruby_llm/mcp/responses/roots_list.rb +0 -31
- data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +0 -50
- data/lib/ruby_llm/mcp/transport.rb +0 -151
- data/lib/ruby_llm/mcp/transports/sse.rb +0 -435
- data/lib/ruby_llm/mcp/transports/stdio.rb +0 -231
- data/lib/ruby_llm/mcp/transports/streamable_http.rb +0 -725
- data/lib/ruby_llm/mcp/transports/support/http_client.rb +0 -28
- data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +0 -47
- data/lib/ruby_llm/mcp/transports/support/timeout.rb +0 -34
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Handlers
|
|
6
|
+
# Promise implementation for async operations
|
|
7
|
+
class Promise
|
|
8
|
+
attr_reader :state, :value, :reason
|
|
9
|
+
|
|
10
|
+
# Initialize a new promise
|
|
11
|
+
def initialize
|
|
12
|
+
@state = :pending
|
|
13
|
+
@value = nil
|
|
14
|
+
@reason = nil
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
@condition = ConditionVariable.new
|
|
17
|
+
@then_callbacks = []
|
|
18
|
+
@catch_callbacks = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Register a callback for successful resolution
|
|
22
|
+
# @param callback [Proc] callback to execute on resolution
|
|
23
|
+
# @return [Promise] returns self for chaining
|
|
24
|
+
def then(&callback)
|
|
25
|
+
should_execute = false
|
|
26
|
+
value_to_use = nil
|
|
27
|
+
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
if @state == :fulfilled
|
|
30
|
+
# Already fulfilled, will execute immediately
|
|
31
|
+
should_execute = true
|
|
32
|
+
value_to_use = @value
|
|
33
|
+
elsif @state == :pending
|
|
34
|
+
# Still pending, register callback
|
|
35
|
+
@then_callbacks << callback
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Execute outside mutex
|
|
40
|
+
execute_callback_sync(callback, value_to_use) if should_execute
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Register a callback for rejection
|
|
45
|
+
# @param callback [Proc] callback to execute on rejection
|
|
46
|
+
# @return [Promise] returns self for chaining
|
|
47
|
+
def catch(&callback)
|
|
48
|
+
should_execute = false
|
|
49
|
+
reason_to_use = nil
|
|
50
|
+
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
if @state == :rejected
|
|
53
|
+
# Already rejected, will execute immediately
|
|
54
|
+
should_execute = true
|
|
55
|
+
reason_to_use = @reason
|
|
56
|
+
elsif @state == :pending
|
|
57
|
+
# Still pending, register callback
|
|
58
|
+
@catch_callbacks << callback
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Execute outside mutex
|
|
63
|
+
execute_callback_sync(callback, reason_to_use) if should_execute
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Resolve the promise with a value
|
|
68
|
+
# @param value [Object] the resolved value
|
|
69
|
+
def resolve(value)
|
|
70
|
+
callbacks_to_execute = nil
|
|
71
|
+
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
return unless @state == :pending
|
|
74
|
+
|
|
75
|
+
@state = :fulfilled
|
|
76
|
+
@value = value
|
|
77
|
+
|
|
78
|
+
# Capture callbacks to execute outside mutex
|
|
79
|
+
callbacks_to_execute = @then_callbacks.dup
|
|
80
|
+
@then_callbacks.clear
|
|
81
|
+
@catch_callbacks.clear
|
|
82
|
+
|
|
83
|
+
# Signal waiting threads
|
|
84
|
+
@condition.broadcast
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Execute callbacks outside mutex to avoid deadlocks
|
|
88
|
+
# Use synchronous execution to maintain order and allow tests to work
|
|
89
|
+
callbacks_to_execute&.each do |callback|
|
|
90
|
+
execute_callback_sync(callback, value)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Reject the promise with a reason
|
|
95
|
+
# @param reason [Object] the rejection reason
|
|
96
|
+
def reject(reason)
|
|
97
|
+
callbacks_to_execute = nil
|
|
98
|
+
|
|
99
|
+
@mutex.synchronize do
|
|
100
|
+
return unless @state == :pending
|
|
101
|
+
|
|
102
|
+
@state = :rejected
|
|
103
|
+
@reason = reason
|
|
104
|
+
|
|
105
|
+
# Capture callbacks to execute outside mutex
|
|
106
|
+
callbacks_to_execute = @catch_callbacks.dup
|
|
107
|
+
@then_callbacks.clear
|
|
108
|
+
@catch_callbacks.clear
|
|
109
|
+
|
|
110
|
+
# Signal waiting threads
|
|
111
|
+
@condition.broadcast
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Execute callbacks outside mutex to avoid deadlocks
|
|
115
|
+
# Use synchronous execution to maintain order and allow tests to work
|
|
116
|
+
callbacks_to_execute&.each do |callback|
|
|
117
|
+
execute_callback_sync(callback, reason)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Check if promise is pending
|
|
122
|
+
def pending?
|
|
123
|
+
@state == :pending
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Check if promise is fulfilled
|
|
127
|
+
def fulfilled?
|
|
128
|
+
@state == :fulfilled
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Check if promise is rejected
|
|
132
|
+
def rejected?
|
|
133
|
+
@state == :rejected
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check if promise is settled (fulfilled or rejected)
|
|
137
|
+
def settled?
|
|
138
|
+
!pending?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Wait for promise to settle with optional timeout
|
|
142
|
+
# @param timeout [Numeric, nil] timeout in seconds
|
|
143
|
+
# @return [Object] resolved value or raises error
|
|
144
|
+
def wait(timeout: nil)
|
|
145
|
+
@mutex.synchronize do
|
|
146
|
+
# Wait until promise is settled
|
|
147
|
+
if timeout
|
|
148
|
+
deadline = Time.now + timeout
|
|
149
|
+
while pending?
|
|
150
|
+
remaining = deadline - Time.now
|
|
151
|
+
if remaining <= 0
|
|
152
|
+
raise Timeout::Error, "Promise timed out after #{timeout} seconds"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
@condition.wait(@mutex, remaining)
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
@condition.wait(@mutex) while pending?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Return value or raise error
|
|
162
|
+
return @value if fulfilled?
|
|
163
|
+
raise @reason if rejected?
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
# Execute a callback safely
|
|
170
|
+
# Note: Executes in background thread to avoid blocking
|
|
171
|
+
def execute_callback(callback, arg)
|
|
172
|
+
Thread.new do
|
|
173
|
+
callback.call(arg)
|
|
174
|
+
rescue StandardError => e
|
|
175
|
+
RubyLLM::MCP.logger.error(
|
|
176
|
+
"Error in promise callback: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Execute a callback synchronously (for testing and immediate execution)
|
|
182
|
+
def execute_callback_sync(callback, arg)
|
|
183
|
+
callback.call(arg)
|
|
184
|
+
rescue StandardError => e
|
|
185
|
+
RubyLLM::MCP.logger.error(
|
|
186
|
+
"Error in promise callback: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Handlers
|
|
6
|
+
# Base class for sampling request handlers
|
|
7
|
+
# Provides access to sample object, guards, and helper methods
|
|
8
|
+
#
|
|
9
|
+
# @example Basic sampling handler
|
|
10
|
+
# class MySamplingHandler < RubyLLM::MCP::Handlers::SamplingHandler
|
|
11
|
+
# def execute
|
|
12
|
+
# response = default_chat_completion("gpt-4")
|
|
13
|
+
# accept(response)
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example Sampling handler with guards
|
|
18
|
+
# class GuardedSamplingHandler < RubyLLM::MCP::Handlers::SamplingHandler
|
|
19
|
+
# allow_models "gpt-4", "claude-3-opus"
|
|
20
|
+
#
|
|
21
|
+
# guard :check_model
|
|
22
|
+
# guard :check_token_limit
|
|
23
|
+
#
|
|
24
|
+
# def execute
|
|
25
|
+
# response = default_chat_completion(sample.model)
|
|
26
|
+
# accept(response)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# private
|
|
30
|
+
#
|
|
31
|
+
# def check_model
|
|
32
|
+
# return true if model_allowed?(sample.model)
|
|
33
|
+
# "Model not allowed"
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# def check_token_limit
|
|
37
|
+
# return true if sample.max_tokens <= 4000
|
|
38
|
+
# "Too many tokens"
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
class SamplingHandler
|
|
42
|
+
include Concerns::Options
|
|
43
|
+
include Concerns::Lifecycle
|
|
44
|
+
include Concerns::Logging
|
|
45
|
+
include Concerns::ErrorHandling
|
|
46
|
+
include Concerns::GuardChecks
|
|
47
|
+
include Concerns::ModelFiltering
|
|
48
|
+
include Concerns::SamplingActions
|
|
49
|
+
|
|
50
|
+
attr_reader :coordinator
|
|
51
|
+
|
|
52
|
+
# Initialize sampling handler
|
|
53
|
+
# @param sample [RubyLLM::MCP::Sample] the sampling request
|
|
54
|
+
# @param coordinator [Object] the coordinator managing the request
|
|
55
|
+
# @param options [Hash] handler-specific options
|
|
56
|
+
def initialize(sample:, coordinator:, **options)
|
|
57
|
+
@sample = sample
|
|
58
|
+
@coordinator = coordinator
|
|
59
|
+
super(**options)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Handlers
|
|
6
|
+
# Detect if a handler is a class (vs a block/proc/lambda)
|
|
7
|
+
# @param handler [Object] the handler to check
|
|
8
|
+
# @return [Boolean] true if handler is a class
|
|
9
|
+
def self.handler_class?(handler)
|
|
10
|
+
handler.is_a?(Class) || (handler.respond_to?(:new) && !handler.respond_to?(:call))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
# Wraps server-initiated requests to support cancellation.
|
|
7
|
+
# The operation tracks terminal state so cancellation outcomes are explicit.
|
|
8
|
+
class CancellableOperation
|
|
9
|
+
attr_reader :request_id, :thread, :state
|
|
10
|
+
|
|
11
|
+
def initialize(request_id)
|
|
12
|
+
@request_id = request_id
|
|
13
|
+
@state = :pending
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@thread = nil
|
|
16
|
+
@result = nil
|
|
17
|
+
@error = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def cancelled?
|
|
21
|
+
@mutex.synchronize { %i[cancelling cancelled].include?(@state) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Symbol] :cancelled, :already_cancelled, :already_completed
|
|
25
|
+
def cancel
|
|
26
|
+
thread_to_cancel = nil
|
|
27
|
+
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
case @state
|
|
30
|
+
when :cancelled, :cancelling
|
|
31
|
+
return :already_cancelled
|
|
32
|
+
when :completed
|
|
33
|
+
return :already_completed
|
|
34
|
+
when :pending
|
|
35
|
+
@state = :cancelled
|
|
36
|
+
return :cancelled
|
|
37
|
+
when :running
|
|
38
|
+
@state = :cancelling
|
|
39
|
+
thread_to_cancel = @thread
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if thread_to_cancel&.alive?
|
|
44
|
+
thread_to_cancel.raise(
|
|
45
|
+
Errors::RequestCancelled.new(
|
|
46
|
+
message: "Request #{@request_id} was cancelled",
|
|
47
|
+
request_id: @request_id
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
else
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
@state = :cancelled if @state == :cancelling
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
:cancelled
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Execute a block in a separate thread so cancellation can interrupt execution.
|
|
60
|
+
# Returns the block result or re-raises non-cancellation exceptions.
|
|
61
|
+
def execute(&)
|
|
62
|
+
return nil if cancelled?
|
|
63
|
+
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
return nil if %i[cancelled cancelling].include?(@state)
|
|
66
|
+
|
|
67
|
+
@state = :running
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@thread = Thread.new do
|
|
71
|
+
Thread.current.abort_on_exception = false
|
|
72
|
+
begin
|
|
73
|
+
@result = yield
|
|
74
|
+
rescue Errors::RequestCancelled, StandardError => e
|
|
75
|
+
@error = e
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@thread.join
|
|
80
|
+
raise @error if @error && !@error.is_a?(Errors::RequestCancelled)
|
|
81
|
+
|
|
82
|
+
@result
|
|
83
|
+
ensure
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
if @state == :running || @state == :cancelling
|
|
86
|
+
@state = @error.is_a?(Errors::RequestCancelled) ? :cancelled : :completed
|
|
87
|
+
end
|
|
88
|
+
@thread = nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|