lom 0.11.0 → 0.12.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: b3a1b6204446324ec92c63443efc8c9578db7628aa4544f42c435a1605ec05aa
4
- data.tar.gz: 7fa61741e1c85db00a5ab168d2814afcc0a21ba0ba2a914649f7e8ef6f6df815
3
+ metadata.gz: b7080950578c9a8cdcff8a1a6e527d2c53a2981fb05f32099e114ac3a73f9221
4
+ data.tar.gz: 49c5a2a4072773cf445f6ba30ee60acb59fb8745a78f927fda4eb987e60d4efe
5
5
  SHA512:
6
- metadata.gz: 468294c000324a795a2a3cb67de0436c492e4d08caffea7d14bbc47486b81640fb2c352df233771aefa1cc136e71a1dbcc98efe24c49c7b79d6952f824e1787d
7
- data.tar.gz: 4add52468b4bb244a215dfa2ac32244417f3b282d615c19f2dcc30cc97601bd9c678180047da6de881bd9952f6423f3d5ac743d0d575dd80079f9d6ddb3e5432
6
+ metadata.gz: dc64dfa9fcaa777ae4e9dd4dd65e0b3527f2bfa1482389aa83714c1d6bad346a4806a174886c2fc74c1ab3a235bbbbf548d13a978650bc6a1a580eb65d778f17
7
+ data.tar.gz: e79faf8dd9af3b388ccf6b7ee67a084832ad1eadab13bc955838492f6feb9cccbcc341f4dd526bb92fe145fcaffc08e9c1d85b12abe02370fd9c61b8aff0ab29
data/README.md CHANGED
@@ -156,3 +156,46 @@ User.locked(true).manager('jdoe').list
156
156
  # Return list of users (as User instance) without managers
157
157
  User.manager(false).all
158
158
  ~~~
159
+
160
+
161
+ Threaded servers
162
+ ================
163
+
164
+ A `Net::LDAP` handler is not thread safe: each operation stores its
165
+ outcome in the handler itself, so two threads sharing a handler can
166
+ read each other's result. When LOM is used from several threads
167
+ (Puma, background jobs, ...), give it a callable instead of a
168
+ handler: it is called once per thread (or fiber), and the handler it
169
+ builds is reused by that thread.
170
+
171
+ ~~~ruby
172
+ LH = -> { Net::LDAP.connect('ldap://127.0.0.1')
173
+ .tap { it.auth 'uid=root,ou=Admins,dc=example,dc=com',
174
+ 'foobar' } }
175
+ ~~~
176
+
177
+ Without an opened connection, `Net::LDAP` connects and binds for each
178
+ operation. `LOM.open` keeps a single connection for all the operations
179
+ of its block; nested calls reuse it, and if the connection or bind
180
+ fails the block still runs with one connection per operation. It only
181
+ opens when the handler comes from a callable, as an opened connection
182
+ must not be shared between threads.
183
+
184
+ ~~~ruby
185
+ LOM.open { User.locked(true).all }
186
+ ~~~
187
+
188
+ In a Rack application, a middleware gives one connection per request:
189
+
190
+ ~~~ruby
191
+ class LDAPConnection
192
+ def initialize(app) = @app = app
193
+ def call(env) = LOM.open { @app.call(env) }
194
+ end
195
+
196
+ use LDAPConnection
197
+ ~~~
198
+
199
+ `LOM.open` only opens the handler of `LOM.lh`; a mapper given its own
200
+ handler (`lh=` or its own `LH` constant) still uses one connection per
201
+ operation.
data/lib/lom/handler.rb CHANGED
@@ -4,6 +4,19 @@ class LOM
4
4
  using LDAP::Extensions
5
5
 
6
6
 
7
+ # Set the LDAP handler to use
8
+ #
9
+ # It can be a Net::LDAP handler, or a callable (proc, lambda, ...)
10
+ # building one. A Net::LDAP handler is not thread safe, so a
11
+ # callable is required as soon as LOM is used from several threads:
12
+ # it will be called once per thread (or fiber), and the resulting
13
+ # handler reused by that thread.
14
+ #
15
+ # ~~~
16
+ # LOM.lh = -> { Net::LDAP.connect('ldap://127.0.0.1')
17
+ # .tap { it.auth(dn, password) } }
18
+ # ~~~
19
+ #
7
20
  def self.lh=(lh)
8
21
  @lh = lh
9
22
  end
@@ -16,11 +29,131 @@ class LOM
16
29
  # * the LH constant in this scope or parent scope
17
30
  # * the one defined in $lh
18
31
  #
32
+ # If the handler is a callable, the handler built by it for the
33
+ # current thread is returned.
34
+ #
19
35
  def self.lh
20
- @lh || const_get(:LH) || $lh
36
+ materialize_lh(lh_source)
21
37
  end
22
38
 
39
+ # Get the LDAP handler source (handler or callable), see lh
40
+ #
41
+ # @api private
42
+ #
43
+ def self.lh_source
44
+ @lh || (const_defined?(:LH) && const_get(:LH)) || $lh
45
+ end
46
+
47
+ # Resolve a handler source to a usable LDAP handler.
48
+ #
49
+ # A callable is called once per thread (or fiber) and its result
50
+ # cached for that thread, anything else is returned unchanged.
51
+ #
52
+ # @api private
53
+ #
54
+ def self.materialize_lh(src)
55
+ return src unless src.respond_to?(:call)
56
+ (Thread.current[:__lom_lh] ||= {}.compare_by_identity)[src] ||=
57
+ src.call
58
+ end
59
+
60
+ # Run the block with a single LDAP connection opened and bound,
61
+ # which is reused by all the LDAP operations of the block,
62
+ # instead of one connection per operation.
63
+ #
64
+ # This is only done if the handler is a callable (see lh=), as an
65
+ # opened connection must not be shared between threads, otherwise
66
+ # the block is simply run.
67
+ #
68
+ # If the connection or bind fails, the block is still run, falling
69
+ # back to one connection per operation.
70
+ #
71
+ # Nested calls reuse the already opened connection.
72
+ #
73
+ # @return the block value
74
+ #
75
+ def self.open
76
+ src = lh_source
77
+ return yield unless src.respond_to?(:call)
78
+
79
+ lh = materialize_lh(src)
80
+ opened = (Thread.current[:__lom_opened] ||= {}.compare_by_identity)
81
+ return yield if opened[lh]
82
+
83
+ entered = false
84
+ result = nil
85
+ begin
86
+ lh.open do
87
+ code = lh.get_operation_result.code
88
+ next unless code == Net::LDAP::ResultCodeSuccess
89
+ opened[lh] = { depth: 0, dirty: false }
90
+ entered = true
91
+ begin
92
+ result = yield
93
+ ensure
94
+ opened.delete(lh)
95
+ end
96
+ end
97
+ rescue Net::LDAP::Error, SystemCallError, IOError
98
+ raise if entered
99
+ end
100
+ entered ? result : yield
101
+ end
102
+
103
+ # Run the block performing a search on the handler.
104
+ #
105
+ # If the handler connection was opened by LOM.open and the search
106
+ # is left before completion (break, exception, ...), the unread
107
+ # search results would be kept until the end of LOM.open, so the
108
+ # connection is replaced by a new one, once no other search is
109
+ # using it.
110
+ #
111
+ # @api private
112
+ #
113
+ def self.searching(lh)
114
+ state = Thread.current[:__lom_opened]&.[](lh)
115
+ return yield if state.nil?
116
+
117
+ state[:depth] += 1
118
+ done = false
119
+ begin
120
+ yield.tap { done = true }
121
+ ensure
122
+ state[:depth] -= 1
123
+ state[:dirty] ||= !done
124
+ reopen(lh, state) if state[:dirty] && state[:depth].zero?
125
+ end
126
+ end
127
+
128
+ # Replace the connection opened by LOM.open with a new one.
129
+ # If it can't be opened and bound, operations fall back to
130
+ # one connection per operation.
131
+ #
132
+ # @note relies on Net::LDAP internals (@open_connection, @auth,
133
+ # #new_connection) as used by Net::LDAP#open
134
+ #
135
+ # @api private
136
+ #
137
+ def self.reopen(lh, state)
138
+ state[:dirty] = false
139
+ begin
140
+ lh.instance_variable_get(:@open_connection)&.close
141
+ rescue StandardError
142
+ end
143
+ lh.instance_variable_set(:@open_connection, nil)
23
144
 
145
+ conn = lh.send(:new_connection)
146
+ res = conn.bind(lh.instance_variable_get(:@auth))
147
+ if res.result_code == Net::LDAP::ResultCodeSuccess
148
+ lh.instance_variable_set(:@open_connection, conn)
149
+ else
150
+ conn.close
151
+ end
152
+ rescue Net::LDAP::Error, SystemCallError, IOError
153
+ conn&.close rescue nil
154
+ end
155
+
156
+
24
157
  # extend Forwardable
25
158
  #
26
159
  # def self.connect(*args)
@@ -38,6 +171,3 @@ class LOM
38
171
  # def_delegator :@lh, :delete
39
172
 
40
173
  end
41
-
42
-
43
-
data/lib/lom/mapper.rb CHANGED
@@ -114,8 +114,12 @@ module Mapper
114
114
  # * the LH constant in this scope or parent scope
115
115
  # * the one provided by LOM.lh
116
116
  #
117
+ # If the handler is a callable, the handler built by it for the
118
+ # current thread is returned (see LOM.lh=).
119
+ #
117
120
  def lh
118
- @__ldap_lh || const_get(:LH) || LOM.lh
121
+ LOM.materialize_lh(@__ldap_lh ||
122
+ (const_defined?(:LH) && const_get(:LH)) || LOM.lh)
119
123
  end
120
124
 
121
125
  # Set the LDAP handler to use
@@ -275,23 +279,30 @@ module Mapper
275
279
  end
276
280
 
277
281
  # Perform search
278
- lh.search(:base => _ldap_branch,
279
- :filter => filter,
280
- :attributes => attributes,
281
- :scope => _ldap_scope) {|entry|
282
-
283
- if rawfilter && !rawfilter.call(entry)
284
- next
285
- elsif paged.nil?
286
- yield(converter.(entry))
287
- elsif skip > 0
288
- skip -= 1
289
- elsif count <= 0
290
- break
291
- else
292
- count -= 1
293
- yield(converter.(entry))
294
- end
282
+ # Note: the search must not be left early (with break), as it
283
+ # would leave unread results on a connection opened
284
+ # by LOM.open; leaving it from the caller block is
285
+ # handled by LOM.searching
286
+ h = lh
287
+ LOM.searching(h) {
288
+ h.search(:base => _ldap_branch,
289
+ :filter => filter,
290
+ :attributes => attributes,
291
+ :scope => _ldap_scope) {|entry|
292
+
293
+ if rawfilter && !rawfilter.call(entry)
294
+ next
295
+ elsif paged.nil?
296
+ yield(converter.(entry))
297
+ elsif skip > 0
298
+ skip -= 1
299
+ elsif count <= 0
300
+ next
301
+ else
302
+ count -= 1
303
+ yield(converter.(entry))
304
+ end
305
+ }
295
306
  }
296
307
 
297
308
  # Return self
data/lib/lom/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class LOM
2
- VERSION = '0.11.0'
2
+ VERSION = '0.12.0'
3
3
  end
data/lom.gemspec CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
26
26
  s.add_dependency 'net-ldap'
27
27
  s.add_development_dependency 'yard', '~>0'
28
28
  s.add_development_dependency 'rake', '~>13'
29
+ s.add_development_dependency 'minitest', '~>5'
29
30
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stéphane D'Alu
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '13'
54
+ - !ruby/object:Gem::Dependency
55
+ name: minitest
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '5'
54
68
  description: |2+
55
69
 
56
70
  Creation of mapping between ldap entry and ruby object.
@@ -92,7 +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.6.9
109
+ rubygems_version: 4.0.21
96
110
  specification_version: 4
97
111
  summary: LDAP Object Mapper
98
112
  test_files: []