Class: CruFlags::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/cru_flags/client.rb

Overview

The polling flag client (design doc §3–§8). One instance per process in normal use, via the CruFlags module singleton.

Constant Summary collapse

VALID_SCHEMES =
%w[http https].freeze
MODES =
%w[background on-demand].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url: nil, poll_seconds: 30.0, fetch_timeout: 2.0, on_error: nil, refresh_mode: nil) ⇒ Client

Returns a new instance of Client.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/cru_flags/client.rb', line 24

def initialize(url: nil, poll_seconds: 30.0, fetch_timeout: 2.0,
  on_error: nil, refresh_mode: nil)
  @poll_seconds = positive(poll_seconds, 30.0)
  @fetch_timeout = positive(fetch_timeout, 2.0)
  @on_error = on_error || default_on_error
  @refresh_mode = resolve_mode(refresh_mode)
  @url = url || self.class.url_from_env
  @document = nil
  @etag = nil
  @healthy = true
  @attempted = false
  @last_attempt_at = nil
  @last_attempt_ok = false
  @closed = false
  @write_mutex = Mutex.new
  @fetch_mutex = Mutex.new
  @ready_mutex = Mutex.new
  @ready_cv = ConditionVariable.new
  @wake = Queue.new
  @thread = nil
  @pid = nil
  validate_url!
end

Class Method Details

.url_from_envObject

The one place CRU_FLAGS_URL is read and normalized (design doc §3: url: nil means "read CRU_FLAGS_URL on first use"). It lives on the Client, not on the module singleton, so a directly-constructed Client honors the documented contract instead of being silently inert; the module singleton gets the same normalization by construction.



19
20
21
22
# File 'lib/cru_flags/client.rb', line 19

def self.url_from_env
  value = ENV[ENV_VAR].to_s.strip
  value.empty? ? nil : value
end

Instance Method Details

#closeObject



117
118
119
120
121
122
123
124
125
126
127
# File 'lib/cru_flags/client.rb', line 117

def close
  @closed = true
  @wake.push(:stop)
  @ready_mutex.synchronize { @ready_cv.broadcast }
  begin
    @thread&.join(0.1)
  rescue
    nil # close called from the poller thread itself must not raise ThreadError
  end
  nil
end

#enabled?(name) ⇒ Boolean

Returns:

  • (Boolean)


50
51
52
53
54
55
56
# File 'lib/cru_flags/client.rb', line 50

def enabled?(name)
  read_path_touch
  doc = @document
  doc&.dig("Flags", name.to_s, "Enabled") == true
rescue
  false
end

#flags_for_adapterObject

The Flipper adapter's read primitive: the frozen Flags hash (or {}), through the same read path as enabled? (lazy start / on-demand refresh).



69
70
71
72
73
74
# File 'lib/cru_flags/client.rb', line 69

def flags_for_adapter
  read_path_touch
  @document&.fetch("Flags", nil) || {}
rescue
  {}
end

#inert?Boolean

Returns:

  • (Boolean)


48
# File 'lib/cru_flags/client.rb', line 48

def inert? = @url.nil?

#ready(timeout: nil) ⇒ Object

Blocks until the first fetch attempt completes (success or failure) and returns whether it did within timeout. Immediately false for inert clients (design doc §3.3, §7).



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/cru_flags/client.rb', line 79

def ready(timeout: nil)
  return false if inert? || (@closed && !@attempted)
  ensure_started
  return on_demand_ready if on_demand?
  @ready_mutex.synchronize do
    deadline = timeout && Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
    until @attempted
      ensure_started # self-heals a poller that died mid-wait, rather than waiting on it forever
      remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
      return false if remaining && remaining <= 0
      @ready_cv.wait(@ready_mutex, remaining || 1.0)
      return false if @closed && !@attempted
    end
    true
  end
rescue
  false
end

#refresh(force: false) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/cru_flags/client.rb', line 98

def refresh(force: false)
  return false if inert? || @closed
  waited_from = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  begin
    @fetch_mutex.synchronize do
      if force
        attempt_fetch unless @last_attempt_at && @last_attempt_at >= waited_from
      elsif stale?
        attempt_fetch
      end
    end
  ensure
    signal_ready
  end
  @attempted && @last_attempt_ok
rescue
  false
end

#snapshotObject



58
59
60
61
62
63
64
# File 'lib/cru_flags/client.rb', line 58

def snapshot
  read_path_touch
  doc = @document
  doc ? JSON.parse(JSON.generate(doc)) : {}
rescue
  {}
end