umbrellio-sequel-plugins 0.17.2 → 0.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a04b57476dd8a38bb6ba7a40a38603ce3dfa807f0a8072c76f4446081b48f97e
4
- data.tar.gz: 89fc7a911c67c32321cb0290b5a408effaf948717f0d60ceaa5437c8f7718006
3
+ metadata.gz: 7a801a66fb801ece2e3db9f48a042d34f9a5f857817cd70103b730bb5025d0fd
4
+ data.tar.gz: e90f785e58bf727159b83bbb0387351254279412fc2e3e7bbd41f1c33ef4767c
5
5
  SHA512:
6
- metadata.gz: 4bdbfa1d179d94da6b41d8f1f17556ec1617d420f23c91a0d84d4cc1e3d56b5bb63500018f482ecb92ff459737e62f3692190a3932f7006c505de182deb6b0e2
7
- data.tar.gz: 4fd0e9eaa31182ed0df4c7c4c7afa32d231f2d89b59621399558fce7145553889cfc784ba02a13f4c1ff50a6453c002d5d1e5e24435f916b3bc425eae2293d77
6
+ metadata.gz: 949c5b6918b91001653bda2706900c9fec8eba2449425e783ba0a593e6bf938443a5723687e2c58cd887700ebeb8167eac59088105e2d12bc2668ca777246aa5
7
+ data.tar.gz: 2635f3b88224877118eb0ffe706e49d32753ab017309b643b661ea9c7e56ad667e9586c4246aa76a7b5d6c8ee49b4a1c9f41768a9e1acef3c96dc9a732ef8de2
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- umbrellio-sequel-plugins (0.17.2)
4
+ umbrellio-sequel-plugins (0.18.0)
5
+ concurrent-ruby
5
6
  sequel
6
7
 
7
8
  GEM
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Sequel
6
+ # https://github.com/jeremyevans/sequel/blob/master/lib/sequel/extensions/async_thread_pool.rb
7
+ module Database::ConcurrentThreadPool
8
+ # Base proxy: delegates all method calls to the resolved async value.
9
+ class BaseProxy < BasicObject
10
+ def method_missing(...)
11
+ __value.public_send(...)
12
+ end
13
+
14
+ def respond_to_missing?(*args)
15
+ __value.respond_to?(*args)
16
+ end
17
+
18
+ [:!, :==, :!=, :instance_eval, :instance_exec].each do |method|
19
+ define_method(method) do |*args, &block|
20
+ __value.public_send(method, *args, &block)
21
+ end
22
+ end
23
+ end
24
+
25
+ # Default proxy: schedules block via Concurrent::Future, blocks on first access.
26
+ class Proxy < BaseProxy
27
+ def initialize(executor, &block)
28
+ super()
29
+
30
+ @future = Concurrent::Promises.future_on(executor, &block)
31
+ end
32
+
33
+ def __value
34
+ @future.value!
35
+ end
36
+ end
37
+
38
+ # Preemptable proxy: calling thread runs the block if the pool hasn't started it yet.
39
+ class PreemptableProxy < BaseProxy
40
+ def initialize(executor, &block)
41
+ super()
42
+
43
+ @mutex = Mutex.new
44
+ @block = block
45
+ @done = false
46
+ @result = nil
47
+ @error = nil
48
+
49
+ executor.post { __run }
50
+ end
51
+
52
+ def __value
53
+ error, result = @mutex.synchronize do
54
+ __execute unless @done
55
+ [@error, @result]
56
+ end
57
+ ::Kernel.raise error if error
58
+ result
59
+ end
60
+
61
+ private
62
+
63
+ def __run
64
+ @mutex.synchronize { __execute unless @done }
65
+ end
66
+
67
+ def __execute
68
+ @result = @block.call
69
+ rescue StandardError => error
70
+ @error = error
71
+ ensure
72
+ @done = true
73
+ end
74
+ end
75
+
76
+ module DatabaseMethods
77
+ class << self
78
+ def make_finalizer(executor) = proc { executor.shutdown }
79
+
80
+ def extended(db)
81
+ db.instance_exec do
82
+ case pool.pool_type
83
+ when :single, :sharded_single
84
+ raise Error, "cannot load concurrent_thread_pool extension " \
85
+ "if using single or sharded_single connection pool"
86
+ end
87
+
88
+ executor, owned = choose_executor(opts)
89
+ proxy_klass =
90
+ typecast_value_boolean(opts[:preempt_async_thread]) ? PreemptableProxy : Proxy
91
+
92
+ define_singleton_method(:async_job_class) { proxy_klass }
93
+ define_singleton_method(:async_thread_executor) { executor }
94
+
95
+ finalizer =
96
+ Sequel::Database::ConcurrentThreadPool::DatabaseMethods.make_finalizer(executor)
97
+ ObjectSpace.define_finalizer(db, finalizer) if owned
98
+
99
+ extend_datasets(DatasetMethods)
100
+ end
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def choose_executor(opts)
107
+ if opts[:async_thread_executor]
108
+ [opts[:async_thread_executor], false]
109
+ else
110
+ num = opts[:num_async_threads] ? typecast_value_integer(opts[:num_async_threads]) :
111
+ Integer(opts[:max_connections] || 4)
112
+ raise Error, "must have positive number for num_async_threads" if num <= 0
113
+ [Concurrent::ThreadPoolExecutor.new(
114
+ min_threads: num,
115
+ max_threads: num,
116
+ max_queue: 0,
117
+ fallback_policy: :abort,
118
+ ), true]
119
+ end
120
+ end
121
+
122
+ def async_run(&block)
123
+ async_job_class.new(async_thread_executor, &block)
124
+ end
125
+ end
126
+
127
+ ASYNC_METHODS = ((
128
+ [:all?, :any?, :drop, :entries, :grep_v, :include?, :inject, :member?, :minmax,
129
+ :none?, :one?, :reduce, :sort, :take, :tally, :to_a, :to_h, :uniq, :zip] &
130
+ Enumerable.instance_methods
131
+ ) + (Dataset::ACTION_METHODS - [:map, :paged_each])).freeze
132
+
133
+ ASYNC_BLOCK_METHODS = ((
134
+ [:collect, :collect_concat, :detect, :drop_while, :each_cons, :each_entry, :each_slice,
135
+ :each_with_index, :each_with_object, :filter_map, :find, :find_all, :find_index,
136
+ :flat_map, :max_by, :min_by, :minmax_by, :partition, :reject, :reverse_each,
137
+ :sort_by, :take_while] & Enumerable.instance_methods
138
+ ) + [:paged_each]).freeze
139
+
140
+ ASYNC_ARGS_OR_BLOCK_METHODS = [:map].freeze
141
+
142
+ module DatasetMethods
143
+ def self.define_async_method(mod, method)
144
+ mod.send(:define_method, method) do |*args, &block|
145
+ if @opts[:async]
146
+ ds = sync
147
+ db.send(:async_run) { ds.send(method, *args, &block) }
148
+ else
149
+ super(*args, &block)
150
+ end
151
+ end
152
+ end
153
+
154
+ def self.define_async_block_method(mod, method)
155
+ mod.send(:define_method, method) do |*args, &block|
156
+ if block && @opts[:async]
157
+ ds = sync
158
+ db.send(:async_run) { ds.send(method, *args, &block) }
159
+ else
160
+ super(*args, &block)
161
+ end
162
+ end
163
+ end
164
+
165
+ def self.define_async_args_or_block_method(mod, method)
166
+ mod.send(:define_method, method) do |*args, &block|
167
+ if (block || !args.empty?) && @opts[:async]
168
+ ds = sync
169
+ db.send(:async_run) { ds.send(method, *args, &block) }
170
+ else
171
+ super(*args, &block)
172
+ end
173
+ end
174
+ end
175
+
176
+ ASYNC_METHODS.each { |m| define_async_method(self, m) }
177
+ ASYNC_BLOCK_METHODS.each { |m| define_async_block_method(self, m) }
178
+ ASYNC_ARGS_OR_BLOCK_METHODS.each { |m| define_async_args_or_block_method(self, m) }
179
+
180
+ def async
181
+ cached_dataset(:_async) { clone(async: true) }
182
+ end
183
+
184
+ def sync
185
+ cached_dataset(:_sync) { clone(async: false) }
186
+ end
187
+ end
188
+ end
189
+
190
+ Database.register_extension(
191
+ :concurrent_thread_pool, Database::ConcurrentThreadPool::DatabaseMethods
192
+ )
193
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # Concurrent eager loading using concurrent_thread_pool extension.
6
+ # Adapted from Sequel's built-in concurrent_eager_loading plugin but uses
7
+ # our concurrent_thread_pool extension instead of async_thread_pool.
8
+ #
9
+ # Usage:
10
+ #
11
+ # DB.extension(:concurrent_thread_pool)
12
+ # Album.plugin :concurrent_thread_pool_eager_loading
13
+ # Album.eager_load_concurrently.eager(:artist, :genre, :tracks).all
14
+ #
15
+ # # Always concurrent by default:
16
+ # Album.plugin :concurrent_thread_pool_eager_loading, always: true
17
+ module ConcurrentThreadPoolEagerLoading
18
+ def self.configure(mod, opts = OPTS)
19
+ if opts.key?(:always)
20
+ mod.instance_variable_set(:@always_eager_load_concurrently, opts[:always])
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ Plugins.inherited_instance_variables(self, :@always_eager_load_concurrently => nil)
26
+ Plugins.def_dataset_methods(self, [:eager_load_concurrently, :eager_load_serially])
27
+
28
+ def always_eager_load_concurrently?
29
+ @always_eager_load_concurrently
30
+ end
31
+ end
32
+
33
+ module DatasetMethods
34
+ def eager_load_concurrently
35
+ cached_dataset(:_eager_load_concurrently) do
36
+ clone(eager_load_concurrently: true)
37
+ end
38
+ end
39
+
40
+ def eager_load_serially
41
+ cached_dataset(:_eager_load_serially) do
42
+ clone(eager_load_concurrently: false)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def eager_load_concurrently?
49
+ v = @opts[:eager_load_concurrently]
50
+ v.nil? ? model.always_eager_load_concurrently? : v
51
+ end
52
+
53
+ def perform_eager_loads(eager_load_data)
54
+ return super if !eager_load_concurrently? || eager_load_data.length < 2
55
+
56
+ mutex = Mutex.new
57
+ eager_load_data.each_value do |elo|
58
+ elo[:mutex] = mutex
59
+ end
60
+
61
+ super.each do |v|
62
+ if Sequel::Database::ConcurrentThreadPool::BaseProxy === v # rubocop:disable Style/CaseEquality
63
+ v.__value
64
+ end
65
+ end
66
+ end
67
+
68
+ def perform_eager_load(loader, elo)
69
+ elo[:mutex] ? db.send(:async_run) { super } : super
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "umbrellio-sequel-plugins"
8
- spec.version = "0.17.2"
8
+ spec.version = "0.18.0"
9
9
  spec.required_ruby_version = ">= 3.0"
10
10
 
11
11
  spec.authors = ["Team Umbrellio"]
@@ -18,5 +18,6 @@ Gem::Specification.new do |spec|
18
18
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_runtime_dependency "concurrent-ruby"
21
22
  spec.add_runtime_dependency "sequel"
22
23
  end
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: umbrellio-sequel-plugins
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.2
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Team Umbrellio
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-03-05 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: concurrent-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: sequel
15
28
  requirement: !ruby/object:Gem::Requirement
@@ -43,6 +56,7 @@ files:
43
56
  - bin/console
44
57
  - bin/setup
45
58
  - lib/clickhouse/migrator.rb
59
+ - lib/sequel/extensions/concurrent_thread_pool.rb
46
60
  - lib/sequel/extensions/currency_rates.rb
47
61
  - lib/sequel/extensions/deferrable_foreign_keys.rb
48
62
  - lib/sequel/extensions/fibered_connection_pool.rb
@@ -54,6 +68,7 @@ files:
54
68
  - lib/sequel/extensions/synchronize.rb
55
69
  - lib/sequel/plugins/attr_encrypted.rb
56
70
  - lib/sequel/plugins/attr_encrypted/simple_crypt.rb
71
+ - lib/sequel/plugins/concurrent_thread_pool_eager_loading.rb
57
72
  - lib/sequel/plugins/duplicate.rb
58
73
  - lib/sequel/plugins/get_column_value.rb
59
74
  - lib/sequel/plugins/money_accessors.rb
@@ -77,7 +92,6 @@ homepage: https://github.com/umbrellio/umbrellio-sequel-plugins
77
92
  licenses:
78
93
  - MIT
79
94
  metadata: {}
80
- post_install_message:
81
95
  rdoc_options: []
82
96
  require_paths:
83
97
  - lib
@@ -92,8 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
106
  - !ruby/object:Gem::Version
93
107
  version: '0'
94
108
  requirements: []
95
- rubygems_version: 3.5.22
96
- signing_key:
109
+ rubygems_version: 4.0.6
97
110
  specification_version: 4
98
111
  summary: Sequel plugins
99
112
  test_files: []