flipper 0.26.0 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -11
  3. data/.github/workflows/examples.yml +5 -10
  4. data/Changelog.md +16 -0
  5. data/Gemfile +6 -3
  6. data/benchmark/enabled_ips.rb +10 -0
  7. data/benchmark/enabled_profile.rb +20 -0
  8. data/benchmark/instrumentation_ips.rb +21 -0
  9. data/benchmark/typecast_ips.rb +19 -0
  10. data/examples/api/basic.ru +3 -4
  11. data/examples/api/custom_memoized.ru +3 -4
  12. data/examples/api/memoized.ru +3 -4
  13. data/flipper.gemspec +0 -2
  14. data/lib/flipper/adapter.rb +23 -7
  15. data/lib/flipper/adapters/http.rb +11 -3
  16. data/lib/flipper/adapters/instrumented.rb +25 -2
  17. data/lib/flipper/adapters/memoizable.rb +19 -2
  18. data/lib/flipper/adapters/memory.rb +56 -39
  19. data/lib/flipper/adapters/operation_logger.rb +16 -3
  20. data/lib/flipper/adapters/poll/poller.rb +2 -125
  21. data/lib/flipper/adapters/poll.rb +4 -0
  22. data/lib/flipper/dsl.rb +1 -5
  23. data/lib/flipper/export.rb +26 -0
  24. data/lib/flipper/exporter.rb +17 -0
  25. data/lib/flipper/exporters/json/export.rb +32 -0
  26. data/lib/flipper/exporters/json/v1.rb +33 -0
  27. data/lib/flipper/feature.rb +22 -18
  28. data/lib/flipper/feature_check_context.rb +4 -4
  29. data/lib/flipper/gate_values.rb +0 -16
  30. data/lib/flipper/gates/actor.rb +2 -12
  31. data/lib/flipper/gates/boolean.rb +1 -1
  32. data/lib/flipper/gates/group.rb +4 -8
  33. data/lib/flipper/gates/percentage_of_actors.rb +9 -11
  34. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  35. data/lib/flipper/instrumentation/subscriber.rb +8 -0
  36. data/lib/flipper/poller.rb +117 -0
  37. data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
  38. data/lib/flipper/test/shared_adapter_test.rb +24 -0
  39. data/lib/flipper/typecast.rb +28 -15
  40. data/lib/flipper/version.rb +1 -1
  41. data/lib/flipper.rb +2 -1
  42. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  43. data/spec/flipper/adapter_spec.rb +29 -2
  44. data/spec/flipper/adapters/http_spec.rb +25 -3
  45. data/spec/flipper/adapters/instrumented_spec.rb +28 -10
  46. data/spec/flipper/adapters/memoizable_spec.rb +30 -10
  47. data/spec/flipper/adapters/memory_spec.rb +3 -1
  48. data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
  49. data/spec/flipper/dsl_spec.rb +20 -3
  50. data/spec/flipper/export_spec.rb +13 -0
  51. data/spec/flipper/exporter_spec.rb +16 -0
  52. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  53. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  54. data/spec/flipper/feature_check_context_spec.rb +12 -12
  55. data/spec/flipper/gate_values_spec.rb +2 -33
  56. data/spec/flipper/gates/percentage_of_actors_spec.rb +1 -1
  57. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -0
  58. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
  59. data/spec/flipper/poller_spec.rb +47 -0
  60. data/spec/flipper/typecast_spec.rb +82 -3
  61. data/spec/flipper_spec.rb +7 -1
  62. data/spec/spec_helper.rb +1 -1
  63. data/spec/support/skippable.rb +18 -0
  64. metadata +25 -3
  65. data/.github/workflows/release.yml +0 -44
@@ -5,8 +5,8 @@ module Flipper
5
5
  # Public: Adapter that wraps another adapter and stores the operations.
6
6
  #
7
7
  # Useful in tests to verify calls and such. Never use outside of testing.
8
- class OperationLogger < SimpleDelegator
9
- include ::Flipper::Adapter
8
+ class OperationLogger
9
+ include Flipper::Adapter
10
10
 
11
11
  class Operation
12
12
  attr_reader :type, :args
@@ -18,6 +18,8 @@ module Flipper
18
18
  end
19
19
 
20
20
  OperationTypes = [
21
+ :import,
22
+ :export,
21
23
  :features,
22
24
  :add,
23
25
  :remove,
@@ -37,7 +39,6 @@ module Flipper
37
39
 
38
40
  # Public
39
41
  def initialize(adapter, operations = nil)
40
- super(adapter)
41
42
  @adapter = adapter
42
43
  @name = :operation_logger
43
44
  @operations = operations || []
@@ -98,6 +99,18 @@ module Flipper
98
99
  @adapter.disable(feature, gate, thing)
99
100
  end
100
101
 
102
+ # Public
103
+ def import(source)
104
+ @operations << Operation.new(:import, [source])
105
+ @adapter.import(source)
106
+ end
107
+
108
+ # Public
109
+ def export(format: :json, version: 1)
110
+ @operations << Operation.new(:export, [format, version])
111
+ @adapter.export(format: format, version: version)
112
+ end
113
+
101
114
  # Public: Count the number of times a certain operation happened.
102
115
  def count(type)
103
116
  type(type).size
@@ -1,125 +1,2 @@
1
- require 'logger'
2
- require 'concurrent/atomic/read_write_lock'
3
- require 'concurrent/utility/monotonic_time'
4
- require 'concurrent/map'
5
-
6
- module Flipper
7
- module Adapters
8
- class Poll
9
- class Poller
10
- attr_reader :thread, :pid, :mutex, :interval, :last_synced_at
11
-
12
- def self.instances
13
- @instances ||= Concurrent::Map.new
14
- end
15
- private_class_method :instances
16
-
17
- def self.get(key, options = {})
18
- instances.compute_if_absent(key) { new(options) }
19
- end
20
-
21
- def self.reset
22
- instances.each {|_,poller| poller.stop }.clear
23
- end
24
-
25
- def initialize(options = {})
26
- @thread = nil
27
- @pid = Process.pid
28
- @mutex = Mutex.new
29
- @adapter = Memory.new
30
- @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
31
- @remote_adapter = options.fetch(:remote_adapter)
32
- @interval = options.fetch(:interval, 10).to_f
33
- @lock = Concurrent::ReadWriteLock.new
34
- @last_synced_at = Concurrent::AtomicFixnum.new(0)
35
-
36
- if @interval < 1
37
- warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
38
- @interval = 1
39
- end
40
-
41
- @start_automatically = options.fetch(:start_automatically, true)
42
-
43
- if options.fetch(:shutdown_automatically, true)
44
- at_exit { stop }
45
- end
46
- end
47
-
48
- def adapter
49
- @lock.with_read_lock { Memory.new(@adapter.get_all.dup) }
50
- end
51
-
52
- def start
53
- reset if forked?
54
- ensure_worker_running
55
- end
56
-
57
- def stop
58
- @instrumenter.instrument("poller.#{InstrumentationNamespace}", {
59
- operation: :stop,
60
- })
61
- @thread&.kill
62
- end
63
-
64
- def run
65
- loop do
66
- sleep jitter
67
- start = Concurrent.monotonic_time
68
- begin
69
- @instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
70
- adapter = Memory.new
71
- adapter.import(@remote_adapter)
72
-
73
- @lock.with_write_lock { @adapter.import(adapter) }
74
- @last_synced_at.update { |time| Concurrent.monotonic_time }
75
- end
76
- rescue => exception
77
- # you can instrument these using poller.flipper
78
- end
79
-
80
- sleep_interval = interval - (Concurrent.monotonic_time - start)
81
- sleep sleep_interval if sleep_interval.positive?
82
- end
83
- end
84
-
85
- private
86
-
87
- def jitter
88
- rand
89
- end
90
-
91
- def forked?
92
- pid != Process.pid
93
- end
94
-
95
- def ensure_worker_running
96
- # Return early if thread is alive and avoid the mutex lock and unlock.
97
- return if thread_alive?
98
-
99
- # If another thread is starting worker thread, then return early so this
100
- # thread can enqueue and move on with life.
101
- return unless mutex.try_lock
102
-
103
- begin
104
- return if thread_alive?
105
- @thread = Thread.new { run }
106
- @instrumenter.instrument("poller.#{InstrumentationNamespace}", {
107
- operation: :thread_start,
108
- })
109
- ensure
110
- mutex.unlock
111
- end
112
- end
113
-
114
- def thread_alive?
115
- @thread && @thread.alive?
116
- end
117
-
118
- def reset
119
- @pid = Process.pid
120
- mutex.unlock if mutex.locked?
121
- end
122
- end
123
- end
124
- end
125
- end
1
+ warn "DEPRECATION WARNING: Flipper::Adapters::Poll::Poller is deprecated. Use Flipper::Poller instead."
2
+ require 'flipper/adapters/poll'
@@ -1,4 +1,5 @@
1
1
  require 'flipper/adapters/sync/synchronizer'
2
+ require 'flipper/poller'
2
3
 
3
4
  module Flipper
4
5
  module Adapters
@@ -6,6 +7,9 @@ module Flipper
6
7
  extend Forwardable
7
8
  include ::Flipper::Adapter
8
9
 
10
+ # Deprecated
11
+ Poller = ::Flipper::Poller
12
+
9
13
  # Public: The name of the adapter.
10
14
  attr_reader :name, :adapter, :poller
11
15
 
data/lib/flipper/dsl.rb CHANGED
@@ -10,7 +10,7 @@ module Flipper
10
10
  # Private: What is being used to instrument all the things.
11
11
  attr_reader :instrumenter
12
12
 
13
- def_delegators :@adapter, :memoize=, :memoizing?
13
+ def_delegators :@adapter, :memoize=, :memoizing?, :import, :export
14
14
 
15
15
  # Public: Returns a new instance of the DSL.
16
16
  #
@@ -272,10 +272,6 @@ module Flipper
272
272
  adapter.features.map { |name| feature(name) }.to_set
273
273
  end
274
274
 
275
- def import(flipper)
276
- adapter.import(flipper.adapter)
277
- end
278
-
279
275
  # Cloud DSL method that does nothing for open source version.
280
276
  def sync
281
277
  end
@@ -0,0 +1,26 @@
1
+ require "flipper/adapters/memory"
2
+
3
+ module Flipper
4
+ class Export
5
+ attr_reader :contents, :format, :version
6
+
7
+ def initialize(contents:, format: :json, version: 1)
8
+ @contents = contents
9
+ @format = format
10
+ @version = version
11
+ end
12
+
13
+ def features
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def adapter
18
+ @adapter ||= Flipper::Adapters::Memory.new(features)
19
+ end
20
+
21
+ def eql?(other)
22
+ self.class.eql?(other.class) && @contents == other.contents && @format == other.format && @version == other.version
23
+ end
24
+ alias_method :==, :eql?
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ require "flipper/exporters/json/v1"
2
+
3
+ module Flipper
4
+ module Exporter
5
+ extend self
6
+
7
+ FORMATTERS = {
8
+ json: {
9
+ 1 => Flipper::Exporters::Json::V1,
10
+ }
11
+ }.freeze
12
+
13
+ def build(format: :json, version: 1)
14
+ FORMATTERS.fetch(format).fetch(version).new
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ require "flipper/export"
2
+ require "flipper/typecast"
3
+
4
+ module Flipper
5
+ module Exporters
6
+ module Json
7
+ # Raised when the contents of the export are not valid.
8
+ class InvalidError < StandardError; end
9
+ class JsonError < InvalidError; end
10
+
11
+ # Internal: JSON export class that knows how to build features hash
12
+ # from data.
13
+ class Export < ::Flipper::Export
14
+ def initialize(contents:, version: 1)
15
+ super contents: contents, version: version, format: :json
16
+ end
17
+
18
+ # Public: The features hash identical to calling get_all on adapter.
19
+ def features
20
+ @features ||= begin
21
+ features = JSON.parse(contents).fetch("features")
22
+ Typecast.features_hash(features)
23
+ rescue JSON::ParserError
24
+ raise JsonError
25
+ rescue
26
+ raise InvalidError
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ require "json"
2
+ require "flipper/exporters/json/export"
3
+
4
+ module Flipper
5
+ module Exporters
6
+ module Json
7
+ class V1
8
+ VERSION = 1
9
+
10
+ def call(adapter)
11
+ features = adapter.get_all
12
+
13
+ # Convert sets to arrays for json
14
+ features.each do |feature_key, gates|
15
+ gates.each do |key, value|
16
+ case value
17
+ when Set
18
+ features[feature_key][key] = value.to_a
19
+ end
20
+ end
21
+ end
22
+
23
+ json = JSON.dump({
24
+ version: VERSION,
25
+ features: features,
26
+ })
27
+
28
+ Json::Export.new(contents: json, version: VERSION)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -100,13 +100,12 @@ module Flipper
100
100
  #
101
101
  # Returns true if enabled, false if not.
102
102
  def enabled?(thing = nil)
103
- instrument(:enabled?) do |payload|
104
- values = gate_values
105
- thing = gate(:actor).wrap(thing) unless thing.nil?
106
- payload[:thing] = thing
103
+ thing = Types::Actor.wrap(thing) unless thing.nil?
104
+
105
+ instrument(:enabled?, thing: thing) do |payload|
107
106
  context = FeatureCheckContext.new(
108
107
  feature_name: @name,
109
- values: values,
108
+ values: gate_values,
110
109
  thing: thing
111
110
  )
112
111
 
@@ -207,7 +206,7 @@ module Flipper
207
206
 
208
207
  if values.boolean || values.percentage_of_time == 100
209
208
  :on
210
- elsif non_boolean_gates.detect { |gate| gate.enabled?(values[gate.key]) }
209
+ elsif non_boolean_gates.detect { |gate| gate.enabled?(values.send(gate.key)) }
211
210
  :conditional
212
211
  else
213
212
  :off
@@ -232,7 +231,8 @@ module Flipper
232
231
 
233
232
  # Public: Returns the raw gate values stored by the adapter.
234
233
  def gate_values
235
- GateValues.new(adapter.get(self))
234
+ adapter_values = adapter.get(self)
235
+ GateValues.new(adapter_values)
236
236
  end
237
237
 
238
238
  # Public: Get groups enabled for this feature.
@@ -290,7 +290,7 @@ module Flipper
290
290
  # Returns an Array of Flipper::Gate instances.
291
291
  def enabled_gates
292
292
  values = gate_values
293
- gates.select { |gate| gate.enabled?(values[gate.key]) }
293
+ gates.select { |gate| gate.enabled?(values.send(gate.key)) }
294
294
  end
295
295
 
296
296
  # Public: Get the names of the enabled gates.
@@ -339,20 +339,24 @@ module Flipper
339
339
  #
340
340
  # Returns an array of gates
341
341
  def gates
342
- @gates ||= [
343
- Gates::Boolean.new,
344
- Gates::Actor.new,
345
- Gates::PercentageOfActors.new,
346
- Gates::PercentageOfTime.new,
347
- Gates::Group.new,
348
- ]
342
+ @gates ||= gates_hash.values.freeze
343
+ end
344
+
345
+ def gates_hash
346
+ @gates_hash ||= {
347
+ boolean: Gates::Boolean.new,
348
+ actor: Gates::Actor.new,
349
+ percentage_of_actors: Gates::PercentageOfActors.new,
350
+ percentage_of_time: Gates::PercentageOfTime.new,
351
+ group: Gates::Group.new,
352
+ }.freeze
349
353
  end
350
354
 
351
355
  # Public: Find a gate by name.
352
356
  #
353
357
  # Returns a Flipper::Gate if found, nil if not.
354
358
  def gate(name)
355
- gates.detect { |gate| gate.name == name.to_sym }
359
+ gates_hash[name.to_sym]
356
360
  end
357
361
 
358
362
  # Public: Find the gate that protects a thing.
@@ -368,8 +372,8 @@ module Flipper
368
372
  private
369
373
 
370
374
  # Private: Instrument a feature operation.
371
- def instrument(operation)
372
- @instrumenter.instrument(InstrumentationName) do |payload|
375
+ def instrument(operation, initial_payload = {})
376
+ @instrumenter.instrument(InstrumentationName, initial_payload) do |payload|
373
377
  payload[:feature_name] = name
374
378
  payload[:operation] = operation
375
379
  payload[:result] = yield(payload) if block_given?
@@ -10,10 +10,10 @@ module Flipper
10
10
  # Public: The thing we want to know if a feature is enabled for.
11
11
  attr_reader :thing
12
12
 
13
- def initialize(options = {})
14
- @feature_name = options.fetch(:feature_name)
15
- @values = options.fetch(:values)
16
- @thing = options.fetch(:thing)
13
+ def initialize(feature_name:, values:, thing:)
14
+ @feature_name = feature_name
15
+ @values = values
16
+ @thing = thing
17
17
  end
18
18
 
19
19
  # Public: Convenience method for groups value like Feature has.
@@ -3,16 +3,6 @@ require 'flipper/typecast'
3
3
 
4
4
  module Flipper
5
5
  class GateValues
6
- # Private: Array of instance variables that are readable through the []
7
- # instance method.
8
- LegitIvars = {
9
- 'boolean' => '@boolean',
10
- 'actors' => '@actors',
11
- 'groups' => '@groups',
12
- 'percentage_of_time' => '@percentage_of_time',
13
- 'percentage_of_actors' => '@percentage_of_actors',
14
- }.freeze
15
-
16
6
  attr_reader :boolean
17
7
  attr_reader :actors
18
8
  attr_reader :groups
@@ -27,12 +17,6 @@ module Flipper
27
17
  @percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time])
28
18
  end
29
19
 
30
- def [](key)
31
- if ivar = LegitIvars[key.to_s]
32
- instance_variable_get(ivar)
33
- end
34
- end
35
-
36
20
  def eql?(other)
37
21
  self.class.eql?(other.class) &&
38
22
  boolean == other.boolean &&
@@ -23,18 +23,8 @@ module Flipper
23
23
  #
24
24
  # Returns true if gate open for thing, false if not.
25
25
  def open?(context)
26
- value = context.values[key]
27
- if context.thing.nil?
28
- false
29
- else
30
- if protects?(context.thing)
31
- actor = wrap(context.thing)
32
- enabled_actor_ids = value
33
- enabled_actor_ids.include?(actor.value)
34
- else
35
- false
36
- end
37
- end
26
+ return false if context.thing.nil?
27
+ context.values.actors.include?(context.thing.value)
38
28
  end
39
29
 
40
30
  def wrap(thing)
@@ -24,7 +24,7 @@ module Flipper
24
24
  # Returns true if explicitly set to true, false if explicitly set to false
25
25
  # or nil if not explicitly set.
26
26
  def open?(context)
27
- context.values[key]
27
+ context.values.boolean
28
28
  end
29
29
 
30
30
  def wrap(thing)
@@ -23,14 +23,10 @@ module Flipper
23
23
  #
24
24
  # Returns true if gate open for thing, false if not.
25
25
  def open?(context)
26
- value = context.values[key]
27
- if context.thing.nil?
28
- false
29
- else
30
- value.any? do |name|
31
- group = Flipper.group(name)
32
- group.match?(context.thing, context)
33
- end
26
+ return false if context.thing.nil?
27
+
28
+ context.values.groups.any? do |name|
29
+ Flipper.group(name).match?(context.thing, context)
34
30
  end
35
31
  end
36
32
 
@@ -21,21 +21,19 @@ module Flipper
21
21
  value > 0
22
22
  end
23
23
 
24
+ # Private: this constant is used to support up to 3 decimal places
25
+ # in percentages.
26
+ SCALING_FACTOR = 1_000
27
+ private_constant :SCALING_FACTOR
28
+
24
29
  # Internal: Checks if the gate is open for a thing.
25
30
  #
26
31
  # Returns true if gate open for thing, false if not.
27
32
  def open?(context)
28
- percentage = context.values[key]
29
-
30
- if Types::Actor.wrappable?(context.thing)
31
- actor = Types::Actor.wrap(context.thing)
32
- id = "#{context.feature_name}#{actor.value}"
33
- # this is to support up to 3 decimal places in percentages
34
- scaling_factor = 1_000
35
- Zlib.crc32(id) % (100 * scaling_factor) < percentage * scaling_factor
36
- else
37
- false
38
- end
33
+ return false if context.thing.nil?
34
+
35
+ id = "#{context.feature_name}#{context.thing.value}"
36
+ Zlib.crc32(id) % (100 * SCALING_FACTOR) < context.values.percentage_of_actors * SCALING_FACTOR
39
37
  end
40
38
 
41
39
  def protects?(thing)
@@ -23,8 +23,7 @@ module Flipper
23
23
  #
24
24
  # Returns true if gate open for thing, false if not.
25
25
  def open?(context)
26
- value = context.values[key]
27
- rand < (value / 100.0)
26
+ rand < (context.values.percentage_of_time / 100.0)
28
27
  end
29
28
 
30
29
  def protects?(thing)
@@ -72,6 +72,14 @@ module Flipper
72
72
  update_timer "flipper.adapter.#{adapter_name}.#{operation}"
73
73
  end
74
74
 
75
+ def update_poller_metrics
76
+ # noop
77
+ end
78
+
79
+ def update_synchronizer_call_metrics
80
+ # noop
81
+ end
82
+
75
83
  QUESTION_MARK = '?'.freeze
76
84
 
77
85
  # Private
@@ -0,0 +1,117 @@
1
+ require 'logger'
2
+ require 'concurrent/utility/monotonic_time'
3
+ require 'concurrent/map'
4
+ require 'concurrent/atomic/atomic_fixnum'
5
+
6
+ module Flipper
7
+ class Poller
8
+ attr_reader :adapter, :thread, :pid, :mutex, :interval, :last_synced_at
9
+
10
+ def self.instances
11
+ @instances ||= Concurrent::Map.new
12
+ end
13
+ private_class_method :instances
14
+
15
+ def self.get(key, options = {})
16
+ instances.compute_if_absent(key) { new(options) }
17
+ end
18
+
19
+ def self.reset
20
+ instances.each {|_,poller| poller.stop }.clear
21
+ end
22
+
23
+ def initialize(options = {})
24
+ @thread = nil
25
+ @pid = Process.pid
26
+ @mutex = Mutex.new
27
+ @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
28
+ @remote_adapter = options.fetch(:remote_adapter)
29
+ @interval = options.fetch(:interval, 10).to_f
30
+ @last_synced_at = Concurrent::AtomicFixnum.new(0)
31
+ @adapter = Adapters::Memory.new
32
+
33
+ if @interval < 1
34
+ warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
35
+ @interval = 1
36
+ end
37
+
38
+ @start_automatically = options.fetch(:start_automatically, true)
39
+
40
+ if options.fetch(:shutdown_automatically, true)
41
+ at_exit { stop }
42
+ end
43
+ end
44
+
45
+ def start
46
+ reset if forked?
47
+ ensure_worker_running
48
+ end
49
+
50
+ def stop
51
+ @instrumenter.instrument("poller.#{InstrumentationNamespace}", {
52
+ operation: :stop,
53
+ })
54
+ @thread&.kill
55
+ end
56
+
57
+ def run
58
+ loop do
59
+ sleep jitter
60
+ start = Concurrent.monotonic_time
61
+ begin
62
+ sync
63
+ rescue => exception
64
+ # you can instrument these using poller.flipper
65
+ end
66
+
67
+ sleep_interval = interval - (Concurrent.monotonic_time - start)
68
+ sleep sleep_interval if sleep_interval.positive?
69
+ end
70
+ end
71
+
72
+ def sync
73
+ @instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
74
+ @adapter.import @remote_adapter
75
+ @last_synced_at.update { |time| Concurrent.monotonic_time }
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def jitter
82
+ rand
83
+ end
84
+
85
+ def forked?
86
+ pid != Process.pid
87
+ end
88
+
89
+ def ensure_worker_running
90
+ # Return early if thread is alive and avoid the mutex lock and unlock.
91
+ return if thread_alive?
92
+
93
+ # If another thread is starting worker thread, then return early so this
94
+ # thread can enqueue and move on with life.
95
+ return unless mutex.try_lock
96
+
97
+ begin
98
+ return if thread_alive?
99
+ @thread = Thread.new { run }
100
+ @instrumenter.instrument("poller.#{InstrumentationNamespace}", {
101
+ operation: :thread_start,
102
+ })
103
+ ensure
104
+ mutex.unlock
105
+ end
106
+ end
107
+
108
+ def thread_alive?
109
+ @thread && @thread.alive?
110
+ end
111
+
112
+ def reset
113
+ @pid = Process.pid
114
+ mutex.unlock if mutex.locked?
115
+ end
116
+ end
117
+ end