Skip to content

Commit 697d761

Browse files
authored
feat: add thread pool and concurrency_max_threads configuration option (#470)
This option allows to limit the maximum number of resources that can be sideloaded concurrently. With a properly configured connection pool, this ensures that the activerecord's connection pool is not exhausted by the sideloading process.
1 parent 18519b6 commit 697d761

File tree

3 files changed

+48
-1
lines changed

3 files changed

+48
-1
lines changed

lib/graphiti/configuration.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ class Configuration
88
# Defaults to false OR if classes are cached (Rails-only)
99
attr_accessor :concurrency
1010

11+
# This number must be considered in accordance with the database
12+
# connection pool size configured in `database.yml`. The connection
13+
# pool should be large enough to accommodate both the foreground
14+
# threads (ie. web server or job worker threads) and background
15+
# threads. For each process, Graphiti will create one global
16+
# executor that uses this many threads to sideload resources
17+
# asynchronously. Thus, the pool size should be at least
18+
# `thread_count + concurrency_max_threads + 1`. For example, if your
19+
# web server has a maximum of 3 threads, and
20+
# `concurrency_max_threads` is set to 4, then your pool size should
21+
# be at least 8.
22+
# @return [Integer] Maximum number of threads to use when fetching sideloads concurrently
23+
attr_accessor :concurrency_max_threads
24+
1125
attr_accessor :respond_to
1226
attr_accessor :context_for_endpoint
1327
attr_accessor :links_on_demand
@@ -26,6 +40,7 @@ class Configuration
2640
def initialize
2741
@raise_on_missing_sideload = true
2842
@concurrency = false
43+
@concurrency_max_threads = 4
2944
@respond_to = [:json, :jsonapi, :xml]
3045
@links_on_demand = false
3146
@pagination_links_on_demand = false

lib/graphiti/scope.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@ module Graphiti
22
class Scope
33
attr_accessor :object, :unpaginated_object
44
attr_reader :pagination
5+
6+
@thread_pool_executor_mutex = Mutex.new
7+
8+
def self.thread_pool_executor
9+
return @thread_pool_executor if @thread_pool_executor
10+
11+
concurrency = Graphiti.config.concurrency_max_threads || 4
12+
@thread_pool_executor_mutex.synchronize do
13+
@thread_pool_executor ||= Concurrent::ThreadPoolExecutor.new(
14+
min_threads: 0,
15+
max_threads: concurrency,
16+
max_queue: concurrency * 4,
17+
fallback_policy: :caller_runs
18+
)
19+
end
20+
end
21+
522
def initialize(object, resource, query, opts = {})
623
@object = object
724
@resource = resource
@@ -49,7 +66,7 @@ def resolve_sideloads(results)
4966
@resource.adapter.close if concurrent
5067
}
5168
if concurrent
52-
promises << Concurrent::Promise.execute(&resolve_sideload)
69+
promises << Concurrent::Promise.execute(executor: self.class.thread_pool_executor, &resolve_sideload)
5370
else
5471
resolve_sideload.call
5572
end

spec/configuration_spec.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,21 @@
150150
end
151151
end
152152

153+
describe "#concurrency_max_threads" do
154+
include_context "with config", :concurrency_max_threads
155+
156+
it "defaults" do
157+
expect(Graphiti.config.concurrency_max_threads).to eq(4)
158+
end
159+
160+
it "is overridable" do
161+
Graphiti.configure do |c|
162+
c.concurrency_max_threads = 1
163+
end
164+
expect(Graphiti.config.concurrency_max_threads).to eq(1)
165+
end
166+
end
167+
153168
describe "#raise_on_missing_sideload" do
154169
include_context "with config", :raise_on_missing_sideload
155170

0 commit comments

Comments
 (0)