Skip to content

Commit 6c797cb

Browse files
authored
Fix runner client race condition (#270)
* Update ruby-lsp requirement * Fix race condition when booting the server
1 parent 7b4de91 commit 6c797cb

File tree

5 files changed

+38
-24
lines changed

5 files changed

+38
-24
lines changed

Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ PATH
55
actionpack (>= 6.0)
66
activerecord (>= 6.0)
77
railties (>= 6.0)
8-
ruby-lsp (>= 0.14.0, < 0.15.0)
8+
ruby-lsp (>= 0.14.2, < 0.15.0)
99
sorbet-runtime (>= 0.5.9897)
1010

1111
GEM
@@ -221,7 +221,7 @@ GEM
221221
rubocop (~> 1.51)
222222
rubocop-sorbet (0.7.7)
223223
rubocop (>= 0.90.0)
224-
ruby-lsp (0.14.1)
224+
ruby-lsp (0.14.2)
225225
language_server-protocol (~> 3.17.0)
226226
prism (>= 0.22.0, < 0.25)
227227
sorbet-runtime (>= 0.5.10782)

lib/ruby_lsp/ruby_lsp_rails/addon.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,24 @@ module Rails
1414
class Addon < ::RubyLsp::Addon
1515
extend T::Sig
1616

17-
sig { returns(RunnerClient) }
18-
def client
19-
@client ||= T.let(RunnerClient.create_client, T.nilable(RunnerClient))
17+
sig { void }
18+
def initialize
19+
super
20+
21+
# We first initialize the client as a NullClient, so that we can start the server in a background thread. Until
22+
# the real client is initialized, features that depend on it will not be blocked by using the NullClient
23+
@client = T.let(NullClient.new, RunnerClient)
2024
end
2125

2226
sig { override.params(message_queue: Thread::Queue).void }
2327
def activate(message_queue)
24-
# Eagerly initialize the client in a thread. This allows the indexing from the Ruby LSP to continue running even
25-
# while we boot large Rails applications in the background
26-
Thread.new { client }
28+
# Start booting the real client in a background thread. Until this completes, the client will be a NullClient
29+
Thread.new { @client = RunnerClient.create_client }
2730
end
2831

2932
sig { override.void }
3033
def deactivate
31-
client.shutdown
34+
@client.shutdown
3235
end
3336

3437
# Creates a new CodeLens listener. This method is invoked on every CodeLens request
@@ -52,7 +55,7 @@ def create_code_lens_listener(response_builder, uri, dispatcher)
5255
).void
5356
end
5457
def create_hover_listener(response_builder, nesting, index, dispatcher)
55-
Hover.new(client, response_builder, nesting, index, dispatcher)
58+
Hover.new(@client, response_builder, nesting, index, dispatcher)
5659
end
5760

5861
sig do

ruby-lsp-rails.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ Gem::Specification.new do |spec|
2424
spec.add_dependency("actionpack", ">= 6.0")
2525
spec.add_dependency("activerecord", ">= 6.0")
2626
spec.add_dependency("railties", ">= 6.0")
27-
spec.add_dependency("ruby-lsp", ">= 0.14.0", "< 0.15.0")
27+
spec.add_dependency("ruby-lsp", ">= 0.14.2", "< 0.15.0")
2828
spec.add_dependency("sorbet-runtime", ">= 0.5.9897")
2929
end

test/ruby_lsp_rails/code_lens_test.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ def generate_code_lens_for_source(source)
207207
store = RubyLsp::Store.new
208208
store.set(uri: uri, source: source, version: 1)
209209

210+
capture_subprocess_io do
211+
RubyLsp::Executor.new(store, @message_queue).execute({
212+
method: "initialized",
213+
params: {},
214+
})
215+
end
216+
210217
response = RubyLsp::Executor.new(store, @message_queue).execute({
211218
method: "textDocument/codeLens",
212219
params: { textDocument: { uri: uri }, position: { line: 0, character: 0 } },

test/ruby_lsp_rails/hover_test.rb

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -195,21 +195,25 @@ def hover_on_source(source, position)
195195
RubyIndexer::IndexablePath.new(nil, T.must(uri.to_standardized_path)), source
196196
)
197197

198-
response = T.let(nil, T.nilable(RubyLsp::Result))
199-
capture_io do
200-
response = executor.execute(
201-
{
202-
method: "textDocument/hover",
203-
params: {
204-
textDocument: { uri: uri },
205-
position: position,
206-
},
207-
},
208-
)
198+
capture_subprocess_io do
199+
RubyLsp::Executor.new(store, @message_queue).execute({
200+
method: "initialized",
201+
params: {},
202+
})
209203
end
210204

211-
assert_nil(T.must(response).error)
212-
T.must(response).response
205+
response = executor.execute(
206+
{
207+
method: "textDocument/hover",
208+
params: {
209+
textDocument: { uri: uri },
210+
position: position,
211+
},
212+
},
213+
)
214+
215+
assert_nil(response.error)
216+
response.response
213217
end
214218

215219
def dummy_root

0 commit comments

Comments
 (0)