Class: ToggleFleet::Client

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

Overview

A self-contained client. Use ToggleFleet.* for the process-wide singleton, or build your own (e.g. to talk to two environments at once): ToggleFleet::Client.new(config).

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Client

Returns a new instance of Client.



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/togglefleet.rb', line 48

def initialize(config)
  @config      = config
  @groups      = {}        # name => predicate proc
  @flags       = {}        # flag key => state hash
  @etag        = nil
  @loaded      = false
  @mutex       = Mutex.new
  @poller      = nil
  @poller_pid  = nil       # pid that owns @poller; threads do not survive fork
  @last_attempt = nil      # monotonic time of the last fetch attempt (success or failure)
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



46
47
48
# File 'lib/togglefleet.rb', line 46

def config
  @config
end

Instance Method Details

#all(actor: nil, groups: nil) ⇒ Object

Snapshot every known flag for an actor — handy for bootstrapping a JS client.



129
130
131
132
133
# File 'lib/togglefleet.rb', line 129

def all(actor: nil, groups: nil)
  ensure_loaded
  keys = @mutex.synchronize { @flags.keys }
  keys.each_with_object({}) { |k, h| h[k] = enabled?(k, actor: actor, groups: groups) }
end

#enabled?(flag, actor: nil, groups: nil) ⇒ Boolean

The whole point: evaluate locally, no network call here.

Returns:

  • (Boolean)


116
117
118
119
120
121
122
123
124
125
126
# File 'lib/togglefleet.rb', line 116

def enabled?(flag, actor: nil, groups: nil)
  restart_poller_if_forked
  ensure_loaded
  state  = @mutex.synchronize { @flags[flag.to_s] }
  result = state ? evaluate(state, actor, groups) : @config.default
  notify_evaluation(flag.to_s, actor, result)
  result
rescue StandardError => e
  log("enabled?(#{flag}) error: #{e.class}: #{e.message}")
  @config.default
end

#register_group(name, &block) ⇒ Object

Register a group predicate. Group membership is decided in YOUR code, so a flag enabled for :admins turns on for any actor where the block returns true.

Raises:

  • (ArgumentError)


62
63
64
65
66
# File 'lib/togglefleet.rb', line 62

def register_group(name, &block)
  raise ArgumentError, "register_group needs a block" unless block
  @mutex.synchronize { @groups[name.to_s] = block }
  self
end

#startObject

Pull the config once and start the background refresh thread. Idempotent.



69
70
71
72
73
# File 'lib/togglefleet.rb', line 69

def start
  attempt_sync
  start_poller
  self
end

#stopObject

Stop the background refresh thread. Safe to call more than once.



76
77
78
79
80
81
82
83
84
85
# File 'lib/togglefleet.rb', line 76

def stop
  thread = @mutex.synchronize do
    current = @poller
    @poller = nil
    @poller_pid = nil
    current
  end
  thread&.kill
  self
end

#syncObject

Force a refresh now (returns true if the config changed).



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/togglefleet.rb', line 136

def sync
  uri = URI.join(@config.url + "/", "v1/config")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == "https"
  http.open_timeout = @config.open_timeout
  http.read_timeout = @config.read_timeout
  req = Net::HTTP::Get.new(uri)
  req["Authorization"] = "Bearer #{@config.sdk_key}"
  req["If-None-Match"] = @etag if @etag
  req["User-Agent"]    = "togglefleet-ruby/#{VERSION}"
  res = http.request(req)

  case res
  when Net::HTTPNotModified
    false
  when Net::HTTPSuccess
    flags = JSON.parse(res.body).fetch("flags", {})
    @mutex.synchronize { @flags = flags; @etag = res["ETag"]; @loaded = true }
    true
  when Net::HTTPUnauthorized
    raise Error, "invalid SDK key (401) — check config.sdk_key"
  else
    raise Error, "config fetch failed: HTTP #{res.code}"
  end
end