legion-cache 1.3.21 → 1.4.1

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.
data/lib/legion/cache.rb CHANGED
@@ -10,33 +10,77 @@ require 'legion/cache/redis'
10
10
  require 'legion/cache/redis_hash'
11
11
  require 'legion/cache/memory'
12
12
  require 'legion/cache/local'
13
+ require 'legion/cache/async_writer'
14
+ require 'legion/cache/reconnector'
15
+ require 'concurrent'
13
16
  require 'legion/cache/helper'
14
17
 
15
18
  module Legion
16
19
  module Cache
17
20
  extend Legion::Logging::Helper
18
21
 
22
+ @async_writer = Legion::Cache::AsyncWriter.new(settings_key: :cache)
23
+ @connected = Concurrent::AtomicBoolean.new(false)
24
+ @using_local = Concurrent::AtomicBoolean.new(false)
25
+ @using_memory = Concurrent::AtomicBoolean.new(false)
26
+
19
27
  class << self
20
28
  include Legion::Logging::Helper
21
29
 
30
+ def enabled?
31
+ return true unless defined?(Legion::Settings)
32
+
33
+ Legion::Settings.dig(:cache, :enabled) != false
34
+ rescue StandardError => e
35
+ handle_exception(e, level: :warn, handled: true, operation: :cache_enabled)
36
+ true
37
+ end
38
+
22
39
  def connected?
23
- @connected == true
40
+ @connected&.true? || false
24
41
  end
25
42
 
26
43
  def driver_name
27
- return 'memory' if @using_memory
28
- return 'local' if @using_local
44
+ return 'memory' if using_memory?
45
+ return 'local' if using_local?
29
46
 
30
47
  @active_shared_driver || configured_shared_driver
31
48
  end
32
49
 
50
+ def stats
51
+ {
52
+ driver: driver_name,
53
+ servers: resolved_servers,
54
+ enabled: enabled?,
55
+ connected: connected?,
56
+ using_local: using_local?,
57
+ using_memory: using_memory?,
58
+ pool_size: safe_pool_size,
59
+ pool_available: safe_pool_available,
60
+ async_pool_size: async_writer_pool_size,
61
+ async_queue_depth: async_writer_queue_depth,
62
+ async_processed: async_writer_processed_count,
63
+ async_failed: async_writer_failed_count,
64
+ reconnect_attempts: reconnector_attempts,
65
+ uptime: uptime_seconds
66
+ }.freeze
67
+ rescue StandardError => e
68
+ handle_exception(e, level: :warn, handled: true, operation: :cache_stats)
69
+ { error: e.message }.freeze
70
+ end
71
+
33
72
  def setup(**)
73
+ return unless enabled?
34
74
  return Legion::Settings[:cache][:connected] = true if connected?
35
75
 
76
+ @setup_at = Time.now
77
+
78
+ async_writer.start
79
+
36
80
  if ENV['LEGION_MODE'] == 'lite'
37
81
  Legion::Cache::Memory.setup
38
- @using_memory = true
39
- @connected = true
82
+ @using_memory.make_true
83
+ @connected.make_true
40
84
  Legion::Settings[:cache][:connected] = true
41
85
  log.info 'Legion::Cache using in-memory adapter (lite mode)'
42
86
  return
@@ -49,15 +93,20 @@ module Legion
49
93
 
50
94
  def shutdown
51
95
  log.info 'Shutting down Legion::Cache'
52
- if @using_memory
96
+ # 1. Drain async writer FIRST (while pool is still alive)
97
+ async_writer.stop(timeout: configured_shutdown_timeout)
98
+ # 2. Stop reconnector
99
+ stop_reconnector
100
+ # 3. Now close pools
101
+ if using_memory?
53
102
  Legion::Cache::Memory.shutdown
54
103
  else
55
- close unless @using_local
104
+ close unless using_local?
56
105
  Legion::Cache::Local.shutdown if Legion::Cache::Local.connected?
57
106
  end
58
- @using_local = false
59
- @using_memory = false
60
- @connected = false
107
+ @using_local.make_false
108
+ @using_memory.make_false
109
+ @connected.make_false
61
110
  Legion::Settings[:cache][:connected] = false
62
111
  end
63
112
 
@@ -65,44 +114,53 @@ module Legion
65
114
  Legion::Cache::Local
66
115
  end
67
116
 
117
+ def pool
118
+ @client
119
+ end
120
+
68
121
  def using_local?
69
- @using_local == true
122
+ @using_local&.true? || false
70
123
  end
71
124
 
72
125
  def using_memory?
73
- @using_memory == true
126
+ @using_memory&.true? || false
74
127
  end
75
128
 
76
129
  def client(**opts)
77
130
  if ENV['LEGION_MODE'] == 'lite'
78
131
  Legion::Cache::Memory.setup unless Legion::Cache::Memory.connected?
79
- @using_memory = true
80
- @using_local = false
81
- @connected = true
132
+ @using_memory.make_true
133
+ @using_local.make_false
134
+ @connected.make_true
82
135
  @active_shared_driver = nil
83
136
  Legion::Settings[:cache][:connected] = true if defined?(Legion::Settings)
84
137
  return Legion::Cache::Memory.client
85
138
  end
86
139
 
87
140
  configure_shared_adapter!(opts[:driver])
88
- @using_memory = false
89
- @using_local = false
141
+ @using_memory.make_false
142
+ @using_local.make_false
90
143
  result = super
91
- @connected = true
144
+ # super (Pool) sets @connected to a plain boolean; restore AtomicBoolean
145
+ @connected = Concurrent::AtomicBoolean.new(true)
92
146
  Legion::Settings[:cache][:connected] = true if defined?(Legion::Settings)
93
147
  result
94
148
  rescue StandardError
95
- @connected = false
149
+ @connected = Concurrent::AtomicBoolean.new(false)
96
150
  Legion::Settings[:cache][:connected] = false if defined?(Legion::Settings)
97
151
  raise
98
152
  end
99
153
 
100
154
  def get(key)
101
- return Legion::Cache::Memory.get(key) if @using_memory
102
- return Legion::Cache::Local.get(key) if @using_local
155
+ return Legion::Cache::Memory.get(key) if using_memory?
156
+ return Legion::Cache::Local.get(key) if using_local?
157
+ return Legion::Cache::Local.get(key) if failback_to_local?
103
158
 
104
159
  configure_shared_adapter!
105
160
  super
161
+ rescue StandardError => e
162
+ handle_exception(e, level: :warn, handled: true, operation: :cache_get, key: key)
163
+ nil
106
164
  end
107
165
 
108
166
  def phi_max_ttl
@@ -121,35 +179,57 @@ module Legion
121
179
  [ttl, max].min
122
180
  end
123
181
 
124
- def set(key, value, ttl = nil, **opts)
125
- ttl = opts.delete(:ttl) || ttl || 180
126
- effective_ttl = enforce_phi_ttl(ttl, **opts)
127
- return Legion::Cache::Memory.set(key, value, effective_ttl) if @using_memory
128
- return Legion::Cache::Local.set(key, value, effective_ttl) if @using_local
182
+ def set(key, value, ttl: nil, async: true, phi: false)
183
+ effective_ttl = resolve_ttl(ttl, phi: phi)
184
+
185
+ if async && async_writer.running?
186
+ async_writer.enqueue { set_internal(key, value, ttl: effective_ttl) }
187
+ true
188
+ else
189
+ set_internal(key, value, ttl: effective_ttl)
190
+ end
191
+ end
192
+
193
+ def set_sync(key, value, ttl: nil, **)
194
+ return Legion::Cache::Memory.set_sync(key, value, ttl: ttl) if using_memory?
195
+ return Legion::Cache::Local.set_sync(key, value, ttl: ttl) if using_local?
196
+ return Legion::Cache::Local.set_sync(key, value, ttl: ttl) if failback_to_local?
129
197
 
130
198
  configure_shared_adapter!
131
- super(key, value, effective_ttl)
199
+ super
132
200
  end
133
201
 
134
- def fetch(key, ttl = nil, &)
135
- return Legion::Cache::Memory.fetch(key, ttl, &) if @using_memory
136
- return Legion::Cache::Local.fetch(key, ttl, &) if @using_local
202
+ def fetch(key, ttl: nil, &)
203
+ return Legion::Cache::Memory.fetch(key, ttl: ttl, &) if using_memory?
204
+ return Legion::Cache::Local.fetch(key, ttl: ttl, &) if using_local?
205
+ return Legion::Cache::Local.fetch(key, ttl: ttl, &) if failback_to_local?
137
206
 
138
207
  configure_shared_adapter!
139
208
  super
140
209
  end
141
210
 
142
- def delete(key)
143
- return Legion::Cache::Memory.delete(key) if @using_memory
144
- return Legion::Cache::Local.delete(key) if @using_local
211
+ def delete(key, async: true)
212
+ if async && async_writer.running?
213
+ async_writer.enqueue { delete_internal(key) }
214
+ true
215
+ else
216
+ delete_internal(key)
217
+ end
218
+ end
219
+
220
+ def delete_sync(key)
221
+ return Legion::Cache::Memory.delete_sync(key) if using_memory?
222
+ return Legion::Cache::Local.delete_sync(key) if using_local?
223
+ return Legion::Cache::Local.delete_sync(key) if failback_to_local?
145
224
 
146
225
  configure_shared_adapter!
147
226
  super
148
227
  end
149
228
 
150
- def flush(delay = 0)
151
- return Legion::Cache::Memory.flush(delay) if @using_memory
152
- return Legion::Cache::Local.flush(delay) if @using_local
229
+ def flush
230
+ return Legion::Cache::Memory.flush if using_memory?
231
+ return Legion::Cache::Local.flush if using_local?
232
+ return Legion::Cache::Local.flush if failback_to_local?
153
233
 
154
234
  configure_shared_adapter!
155
235
  super
@@ -158,35 +238,48 @@ module Legion
158
238
  def mget(*keys)
159
239
  keys = keys.flatten
160
240
  return {} if keys.empty?
161
- return keys.to_h { |key| [key, Legion::Cache::Memory.get(key)] } if @using_memory
162
- return local_mget(*keys) if @using_local
241
+ return keys.to_h { |key| [key, Legion::Cache::Memory.get(key)] } if using_memory?
242
+ return Legion::Cache::Local.mget(*keys) if using_local?
243
+ return Legion::Cache::Local.mget(*keys) if failback_to_local?
163
244
 
164
245
  configure_shared_adapter!
165
246
  super
166
247
  end
167
248
 
168
- def mset(hash)
249
+ def mset(hash, ttl: nil, async: true)
169
250
  return true if hash.empty?
170
- return hash.each { |key, value| Legion::Cache::Memory.set(key, value) } && true if @using_memory
171
- return local_mset(hash) if @using_local
251
+
252
+ if async && async_writer.running?
253
+ async_writer.enqueue { mset_internal(hash, ttl: ttl) }
254
+ true
255
+ else
256
+ mset_internal(hash, ttl: ttl)
257
+ end
258
+ end
259
+
260
+ def mset_sync(hash, ttl: nil, **)
261
+ return true if hash.empty?
262
+ return hash.each { |key, value| Legion::Cache::Memory.set_sync(key, value, ttl: ttl) } && true if using_memory?
263
+ return Legion::Cache::Local.mset(hash, ttl: ttl) if using_local?
264
+ return Legion::Cache::Local.mset(hash, ttl: ttl) if failback_to_local?
172
265
 
173
266
  configure_shared_adapter!
174
267
  super
175
268
  end
176
269
 
177
270
  def close
178
- if @using_memory
271
+ if using_memory?
179
272
  Legion::Cache::Memory.shutdown
180
- @using_memory = false
181
- @connected = false
273
+ @using_memory.make_false
274
+ @connected.make_false
182
275
  Legion::Settings[:cache][:connected] = false if defined?(Legion::Settings)
183
276
  return false
184
277
  end
185
278
 
186
- if @using_local
279
+ if using_local?
187
280
  Legion::Cache::Local.close
188
- @using_local = false
189
- @connected = false
281
+ @using_local.make_false
282
+ @connected.make_false
190
283
  Legion::Settings[:cache][:connected] = false if defined?(Legion::Settings)
191
284
  return false
192
285
  end
@@ -195,48 +288,48 @@ module Legion
195
288
 
196
289
  configure_shared_adapter!
197
290
  result = super
198
- @connected = false
291
+ @connected = Concurrent::AtomicBoolean.new(false)
199
292
  Legion::Settings[:cache][:connected] = false if defined?(Legion::Settings)
200
293
  result
201
294
  end
202
295
 
203
296
  def restart(**opts)
204
297
  configure_shared_adapter!(opts[:driver])
205
- @using_memory = false
206
- @using_local = false
298
+ @using_memory.make_false
299
+ @using_local.make_false
207
300
  result = super
208
- @connected = true
301
+ @connected = Concurrent::AtomicBoolean.new(true)
209
302
  Legion::Settings[:cache][:connected] = true if defined?(Legion::Settings)
210
303
  result
211
304
  end
212
305
 
213
306
  def size
214
- return Legion::Cache::Memory.size if @using_memory
215
- return Legion::Cache::Local.size if @using_local
307
+ return Legion::Cache::Memory.size if using_memory?
308
+ return Legion::Cache::Local.size if using_local?
216
309
 
217
310
  configure_shared_adapter!
218
311
  super
219
312
  end
220
313
 
221
314
  def available
222
- return Legion::Cache::Memory.available if @using_memory
223
- return Legion::Cache::Local.available if @using_local
315
+ return Legion::Cache::Memory.available if using_memory?
316
+ return Legion::Cache::Local.available if using_local?
224
317
 
225
318
  configure_shared_adapter!
226
319
  super
227
320
  end
228
321
 
229
322
  def pool_size
230
- return Legion::Cache::Memory.size if @using_memory
231
- return Legion::Cache::Local.pool_size if @using_local
323
+ return Legion::Cache::Memory.size if using_memory?
324
+ return Legion::Cache::Local.pool_size if using_local?
232
325
 
233
326
  configure_shared_adapter!
234
327
  super
235
328
  end
236
329
 
237
330
  def timeout
238
- return 0 if @using_memory
239
- return Legion::Cache::Local.timeout if @using_local
331
+ return 0 if using_memory?
332
+ return Legion::Cache::Local.timeout if using_local?
240
333
 
241
334
  configure_shared_adapter!
242
335
  super
@@ -244,6 +337,64 @@ module Legion
244
337
 
245
338
  private
246
339
 
340
+ def async_writer
341
+ Legion::Cache.instance_variable_get(:@async_writer)
342
+ end
343
+
344
+ def set_internal(key, value, ttl: nil)
345
+ return Legion::Cache::Memory.set(key, value, ttl: ttl) if using_memory?
346
+ return Legion::Cache::Local.set(key, value, ttl: ttl) if using_local?
347
+ return Legion::Cache::Local.set(key, value, ttl: ttl) if failback_to_local?
348
+
349
+ configure_shared_adapter!
350
+ set_sync(key, value, ttl: ttl)
351
+ end
352
+
353
+ def delete_internal(key)
354
+ return Legion::Cache::Memory.delete(key) if using_memory?
355
+ return Legion::Cache::Local.delete(key) if using_local?
356
+ return Legion::Cache::Local.delete(key) if failback_to_local?
357
+
358
+ configure_shared_adapter!
359
+ delete_sync(key)
360
+ end
361
+
362
+ def mset_internal(hash, ttl: nil)
363
+ return hash.each { |key, value| Legion::Cache::Memory.set(key, value, ttl: ttl) } && true if using_memory?
364
+ return Legion::Cache::Local.mset(hash, ttl: ttl) if using_local?
365
+ return Legion::Cache::Local.mset(hash, ttl: ttl) if failback_to_local?
366
+
367
+ configure_shared_adapter!
368
+ mset_sync(hash, ttl: ttl)
369
+ end
370
+
371
+ def failback_to_local?
372
+ return false unless Legion::Cache::Local.connected?
373
+
374
+ setting = if defined?(Legion::Settings)
375
+ Legion::Settings.dig(:cache, :failback_to_local) != false
376
+ else
377
+ true
378
+ end
379
+ setting && (!enabled? || !connected?)
380
+ rescue StandardError => e
381
+ handle_exception(e, level: :warn, handled: true, operation: :cache_failback_check)
382
+ false
383
+ end
384
+
385
+ def resolve_ttl(ttl, phi: false)
386
+ effective = ttl || default_ttl
387
+ enforce_phi_ttl(effective, phi: phi)
388
+ end
389
+
390
+ def default_ttl
391
+ return 3600 unless defined?(Legion::Settings)
392
+
393
+ Legion::Settings.dig(:cache, :default_ttl) || 3600
394
+ rescue StandardError
395
+ 3600
396
+ end
397
+
247
398
  def setup_local
248
399
  return if Legion::Cache::Local.connected?
249
400
 
@@ -254,8 +405,8 @@ module Legion
254
405
 
255
406
  def setup_shared(**)
256
407
  client(**Legion::Settings[:cache], logger: log, **)
257
- @connected = true
258
- @using_local = false
408
+ @connected.make_true
409
+ @using_local.make_false
259
410
  Legion::Settings[:cache][:connected] = true
260
411
  driver = Legion::Settings[:cache][:driver] || 'dalli'
261
412
  servers = Array(Legion::Settings[:cache][:servers]).join(', ')
@@ -263,15 +414,24 @@ module Legion
263
414
  rescue StandardError => e
264
415
  report_exception(e, level: :warn, handled: true, operation: :setup_shared, fallback: :local)
265
416
  if Legion::Cache::Local.connected?
266
- @using_local = true
267
- @connected = true
417
+ @using_local.make_true
418
+ @connected.make_true
268
419
  Legion::Settings[:cache][:connected] = true
269
420
  log.info 'Legion::Cache fell back to Local cache'
270
421
  else
271
- @connected = false
422
+ @connected.make_false
272
423
  Legion::Settings[:cache][:connected] = false
273
424
  log.error 'Legion::Cache shared and local adapters are unavailable'
274
425
  end
426
+ start_reconnector if enabled?
427
+ end
428
+
429
+ def reconnect_shared!
430
+ client(**Legion::Settings[:cache], logger: log)
431
+ @connected.make_true
432
+ @using_local.make_false
433
+ Legion::Settings[:cache][:connected] = true
434
+ log.info 'Legion::Cache shared reconnected'
275
435
  end
276
436
 
277
437
  def report_exception(exception, level:, handled:, **)
@@ -317,16 +477,96 @@ module Legion
317
477
  handle_exception(e, level: :warn, handled: true, operation: :cache_close_existing_shared_client)
318
478
  ensure
319
479
  @client = nil
320
- @connected = false
480
+ @connected = Concurrent::AtomicBoolean.new(false)
481
+ end
482
+
483
+ def resolved_servers
484
+ return [] if using_memory?
485
+
486
+ Array(Legion::Settings.dig(:cache, :servers))
487
+ rescue StandardError
488
+ []
321
489
  end
322
490
 
323
- def local_mget(*keys)
324
- keys.to_h { |key| [key, Legion::Cache::Local.get(key)] }
491
+ def safe_pool_size
492
+ return 1 if using_memory?
493
+ return 0 unless connected?
494
+
495
+ pool_size
496
+ rescue StandardError
497
+ 0
325
498
  end
326
499
 
327
- def local_mset(hash)
328
- hash.each { |key, value| Legion::Cache::Local.set(key, value) }
329
- true
500
+ def safe_pool_available
501
+ return 1 if using_memory?
502
+ return 0 unless connected?
503
+
504
+ available
505
+ rescue StandardError
506
+ 0
507
+ end
508
+
509
+ def async_writer_pool_size
510
+ async_writer.pool_size
511
+ rescue StandardError
512
+ 0
513
+ end
514
+
515
+ def async_writer_queue_depth
516
+ async_writer.queue_depth
517
+ rescue StandardError
518
+ 0
519
+ end
520
+
521
+ def async_writer_processed_count
522
+ async_writer.processed_count
523
+ rescue StandardError
524
+ 0
525
+ end
526
+
527
+ def configured_shutdown_timeout
528
+ return 5 unless defined?(Legion::Settings)
529
+
530
+ Legion::Settings.dig(:cache, :async, :shutdown_timeout) || 5
531
+ rescue StandardError
532
+ 5
533
+ end
534
+
535
+ def async_writer_failed_count
536
+ async_writer.failed_count
537
+ rescue StandardError
538
+ 0
539
+ end
540
+
541
+ def reconnector_attempts
542
+ @reconnector&.attempts || 0
543
+ end
544
+
545
+ def start_reconnector
546
+ return unless enabled?
547
+
548
+ stop_reconnector
549
+ @reconnector = Legion::Cache::Reconnector.new(
550
+ tier: :shared,
551
+ connect_block: -> { reconnect_shared! },
552
+ enabled_block: -> { enabled? },
553
+ settings_key: :cache
554
+ )
555
+ @reconnector.start
556
+ log.info 'Legion::Cache started background reconnector for shared tier'
557
+ end
558
+
559
+ def stop_reconnector
560
+ @reconnector&.stop
561
+ @reconnector = nil
562
+ end
563
+
564
+ def uptime_seconds
565
+ return 0 unless @setup_at
566
+
567
+ (Time.now - @setup_at).to_i
568
+ rescue StandardError
569
+ 0
330
570
  end
331
571
  end
332
572
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.21
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
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: '1.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.2'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: connection_pool
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -102,12 +116,14 @@ files:
102
116
  - README.md
103
117
  - legion-cache.gemspec
104
118
  - lib/legion/cache.rb
119
+ - lib/legion/cache/async_writer.rb
105
120
  - lib/legion/cache/cacheable.rb
106
121
  - lib/legion/cache/helper.rb
107
122
  - lib/legion/cache/local.rb
108
123
  - lib/legion/cache/memcached.rb
109
124
  - lib/legion/cache/memory.rb
110
125
  - lib/legion/cache/pool.rb
126
+ - lib/legion/cache/reconnector.rb
111
127
  - lib/legion/cache/redis.rb
112
128
  - lib/legion/cache/redis_hash.rb
113
129
  - lib/legion/cache/settings.rb