vpndetection 5.4.2 → 5.4.3

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: 2ccf5841f50ec64ff7328341110f3ec6cd0affc6260a0588fe22cb746d5d62f1
4
- data.tar.gz: 99459a01b5d0139a64d039a2259a08c965cd4319c9cae6283dbbc0b113ac5b2c
3
+ metadata.gz: ba80a879812af8dc9d6cdefdb305d97fe38281550cad1b2c9bd76ab31b416bf2
4
+ data.tar.gz: 602fb1b3b602fa0ec12ca8d465dcda92e22d87610714ca31ec735033a83b61f6
5
5
  SHA512:
6
- metadata.gz: fbdbee3d38ba2e223be9fc4e1cb53193f71e1891f0e59351d4c59f2eb2a8cc418476f48e0b866fb7c0b6d36fbb166686c46c22049deae80403f8a9a0675ab797
7
- data.tar.gz: 94d1e3c266372d5b7fe571ea2d370a38dbd91cd11f184f3f06ef7b3f231ddddbc6a261551185a9020453d0704d8911c6c8b21e6de569cb43d4a4cba562d6684f
6
+ metadata.gz: 340fbe8a1a6265f5dec5d5e93fecd2c63c171af83699fdc073ac98410d83e2ec4924c2b358c4a30785f3aeebb8b606cbefa0eeba0c54f402fda60a03dda1f843
7
+ data.tar.gz: eae9ca20b51fbb6d12060add25b281625c5096e9b54fd7ef1b596a8c54c476589fc09662c117c4eeb879556482fe7caa9056c0b81581af98d739079df544d557
@@ -49,6 +49,9 @@ module VPNDetection
49
49
  Transport::Config.new(api_key: api_key, base_url: base_url, timeout: timeout),
50
50
  )
51
51
  @cache = cache ? Cache.new(max_size: cache_max_size, ttl: cache_ttl) : nil
52
+ # The addresses with a request in flight, each a Flight its waiters block on.
53
+ @flights = {}
54
+ @flights_lock = Mutex.new
52
55
  @concurrency = concurrency
53
56
  @retries = retries
54
57
  @database = DatabaseApi.new(@transport, retries: retries)
@@ -78,14 +81,26 @@ module VPNDetection
78
81
  Transport.checked_timeout(timeout) unless timeout.nil?
79
82
  return Bogon.result(ip) if Bogon.bogon?(ip)
80
83
 
81
- hit = @cache&.get(ip)
82
- return hit unless hit.nil?
84
+ loop do
85
+ hit = @cache&.get(ip)
86
+ return hit unless hit.nil?
83
87
 
84
- result = Retries.with_retries(retries || @retries) do
85
- Transport.lookup_result(@transport.lookup_request(ip, timeout: timeout).run)
88
+ flight, leader = board(ip)
89
+ return fetch(ip, retries, timeout) if flight.nil?
90
+
91
+ if leader
92
+ # Read once more now the address is boarded: a leader that landed
93
+ # between the miss above and boarding has already cached its answer.
94
+ hit = @cache.get(ip)
95
+ return unboard(ip, flight, :served, hit) unless hit.nil?
96
+
97
+ return lead(ip, flight) { fetch(ip, retries, timeout) }
98
+ end
99
+ state, value = flight.wait
100
+ return value if state == :served
101
+ raise value if state == :failed
102
+ # :abandoned - its leader never finished, so this caller asks again.
86
103
  end
87
- @cache&.set(ip, result)
88
- result
89
104
  end
90
105
 
91
106
  # Classify the address this client is calling from.
@@ -160,9 +175,27 @@ module VPNDetection
160
175
  hit = Bogon.bogon?(ip) ? Bogon.result(ip) : @cache&.get(ip)
161
176
  hit.nil? ? pending << ip : answers[ip] = hit
162
177
  end
163
- unless pending.empty?
164
- run_batch(pending.each_slice(BATCH_MAX).to_a, answers, limit, retries || @retries, timeout)
178
+ # Boarded before any chunk is built, so a lookup arriving meanwhile waits
179
+ # for this batch; an address already in flight is not sent again.
180
+ boarded = {}
181
+ joined = {}
182
+ pending.each do |ip|
183
+ flight, leader = board(ip)
184
+ if leader == false
185
+ joined[ip] = flight
186
+ else
187
+ boarded[ip] = flight
188
+ end
189
+ end
190
+ begin
191
+ unless boarded.empty?
192
+ chunks = boarded.keys.each_slice(BATCH_MAX).to_a
193
+ run_batch(chunks, answers, limit, retries || @retries, timeout, boarded)
194
+ end
195
+ ensure
196
+ boarded.each { |ip, flight| unboard(ip, flight, :abandoned) unless flight.nil? }
165
197
  end
198
+ joined.each { |ip, flight| answers[ip] = joined_answer(ip, flight, retries, timeout) }
166
199
 
167
200
  # Reinstated in input order: a hydra settles in completion order, and a
168
201
  # caller iterating the hash should see what they passed in.
@@ -171,11 +204,83 @@ module VPNDetection
171
204
 
172
205
  private
173
206
 
207
+ # Concurrent misses for one address share ONE request (docs/sdk/contract.md,
208
+ # UMAN-4645): 5.4.2 sent one per calling thread. The first miss boards the
209
+ # address and every miss after it, a batch's included, waits on its Flight.
210
+ class Flight
211
+ def initialize
212
+ @lock = Mutex.new
213
+ @landed = ConditionVariable.new
214
+ @state = :pending
215
+ end
216
+
217
+ # Settles the flight once, for every waiter: :served with a Result,
218
+ # :failed with an Error, or :abandoned when its leader never finished.
219
+ def land(state, value = nil)
220
+ @lock.synchronize do
221
+ next unless @state == :pending
222
+
223
+ @state = state
224
+ @value = value
225
+ @landed.broadcast
226
+ end
227
+ end
228
+
229
+ # Blocks until the flight lands, then [state, value].
230
+ def wait
231
+ @lock.synchronize do
232
+ @landed.wait(@lock) while @state == :pending
233
+ [@state, @value]
234
+ end
235
+ end
236
+ end
237
+
238
+ # [flight, true] when this caller leads the request for `ip`, [flight,
239
+ # false] when one is already in flight, and nil without a cache: every
240
+ # lookup is then served, so nothing is shared.
241
+ def board(ip)
242
+ return nil if @cache.nil?
243
+
244
+ @flights_lock.synchronize do
245
+ existing = @flights[ip]
246
+ existing ? [existing, false] : [@flights[ip] = Flight.new, true]
247
+ end
248
+ end
249
+
250
+ # Takes `ip` off the board and lands its flight. Returns `value`.
251
+ def unboard(ip, flight, state, value = nil)
252
+ @flights_lock.synchronize { @flights.delete(ip) if @flights[ip].equal?(flight) }
253
+ flight.land(state, value)
254
+ value
255
+ end
256
+
257
+ # Runs the request a flight is waiting on, under the options of the call
258
+ # that leads it. A served answer is cached before it lands; a failure
259
+ # reaches every waiter and is cached for none; and a leader that never
260
+ # finishes - a killed thread, a Timeout.timeout - abandons the flight, so
261
+ # its waiters ask again rather than failing with it.
262
+ def lead(ip, flight)
263
+ unboard(ip, flight, :served, yield)
264
+ rescue Error => e
265
+ unboard(ip, flight, :failed, e)
266
+ raise
267
+ ensure
268
+ unboard(ip, flight, :abandoned)
269
+ end
270
+
271
+ def fetch(ip, retries, timeout)
272
+ result = Retries.with_retries(retries || @retries) do
273
+ Transport.lookup_result(@transport.lookup_request(ip, timeout: timeout).run)
274
+ end
275
+ @cache&.set(ip, result)
276
+ result
277
+ end
278
+
174
279
  # One hydra per call, sized for THIS call. Reusing an instance-level hydra
175
280
  # would silently cap a per-call concurrency at the client's setting, and
176
281
  # would not be safe to drive from two threads either. Each request is one
177
282
  # chunk of up to 1000 addresses.
178
- def run_batch(chunks, answers, concurrency, retries, timeout)
283
+ def run_batch(chunks, answers, concurrency, retries, timeout, boarded)
179
284
  hydra = Typhoeus::Hydra.new(max_concurrency: concurrency)
180
285
  attempts = Hash.new(0)
181
286
 
@@ -183,7 +288,11 @@ module VPNDetection
183
288
  request = @transport.batch_request(chunk, timeout: timeout)
184
289
  request.on_complete do |response|
185
290
  outcome = settle(chunk, response, attempts, retries, enqueue)
186
- answers.merge!(outcome) unless outcome.nil?
291
+ outcome&.each do |ip, answer|
292
+ answers[ip] = answer
293
+ flight = boarded[ip]
294
+ unboard(ip, flight, answer.is_a?(Result) ? :served : :failed, answer) unless flight.nil?
295
+ end
187
296
  end
188
297
  hydra.queue(request)
189
298
  end
@@ -214,6 +323,17 @@ module VPNDetection
214
323
  nil
215
324
  end
216
325
 
326
+ # What a batch takes for an address another call was fetching: that call's
327
+ # answer or error, or, when its leader never finished, its own lookup.
328
+ def joined_answer(ip, flight, retries, timeout)
329
+ state, value = flight.wait
330
+ return value unless state == :abandoned
331
+
332
+ lookup(ip, retries: retries, timeout: timeout)
333
+ rescue Error => e
334
+ e
335
+ end
336
+
217
337
  # Every address lands in exactly one of `results` and `errors`; an address in
218
338
  # neither is the server breaking its own contract, and is reported as such
219
339
  # rather than lost.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VPNDetection
4
- VERSION = '5.4.2'
4
+ VERSION = '5.4.3'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vpndetection
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.2
4
+ version: 5.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mslm Dev