Class: Feat::Client

Inherits:
Object
  • Object
show all
Includes:
InterruptibleSleep
Defined in:
lib/feat/client.rb

Overview

HTTP client. Uses stdlib only - zero gem dependencies.

By default the client streams datafile updates over Server-Sent Events and keeps a slow background poll as a safety net. Disable streaming with ‘streaming: false` to fall back to polling alone.

Constant Summary collapse

DEFAULT_URL =
"https://data-01.feat.so"
DEFAULT_POLL_INTERVAL =
30.0
DEFAULT_SAFETY_POLL_INTERVAL =

When streaming carries updates, the poll is a backstop only and runs far less often.

300.0
MIN_POLL_INTERVAL =
5.0
MAX_DATAFILE_BYTES =
10 * 1024 * 1024
OPEN_TIMEOUT_SECONDS =
3
READ_TIMEOUT_SECONDS =
10
STREAM_READ_TIMEOUT_SECONDS =

Long-lived stream read: heartbeat comments keep it well under this.

90
POLL_JOIN_TIMEOUT_SECONDS =

Bound on how long #close waits for the poll thread to unwind, so a blocked fetch cannot make shutdown hang indefinitely.

5
RETRYABLE_CONNECT_ERRORS =
[
  Net::OpenTimeout,
  Errno::ETIMEDOUT,
  Errno::ECONNREFUSED,
  Errno::EHOSTUNREACH,
  Errno::ENETUNREACH,
].freeze

Constants included from InterruptibleSleep

InterruptibleSleep::SLEEP_GRANULARITY

Instance Method Summary collapse

Constructor Details

#initialize(api_key:, url: DEFAULT_URL, poll_interval: DEFAULT_POLL_INTERVAL, streaming: true, safety_poll_interval: DEFAULT_SAFETY_POLL_INTERVAL, http_client: nil, stream_transport: nil) ⇒ Client

Returns a new instance of Client.

Raises:

  • (ArgumentError)


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/feat/client.rb', line 40

def initialize(api_key:, url: DEFAULT_URL, poll_interval: DEFAULT_POLL_INTERVAL,
               streaming: true, safety_poll_interval: DEFAULT_SAFETY_POLL_INTERVAL,
               http_client: nil, stream_transport: nil)
  raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?

  assert_https_url!(url)

  @api_key           = api_key
  @url               = url.chomp("/")
  @streaming_enabled = streaming
  base_interval      = streaming ? safety_poll_interval : poll_interval
  @poll_interval     = [base_interval.to_f, MIN_POLL_INTERVAL].max
  @http_client       = http_client
  @datafile          = nil
  @etag              = nil
  @mutex             = Mutex.new
  @stop              = false
  @thread            = nil
  @sticky_ip         = nil
  @streaming         = build_streaming_client(stream_transport) if @streaming_enabled
end

Instance Method Details

#closeObject



70
71
72
73
74
75
76
77
78
# File 'lib/feat/client.rb', line 70

def close
  @stop = true
  @streaming&.stop
  # Join the poll thread (bounded) so a fetch in flight is waited out and
  # @thread is cleared, leaving the client cleanly restartable.
  @thread&.join(POLL_JOIN_TIMEOUT_SECONDS)
  @thread = nil
  self
end

#evaluate(flag_key, default_value, ctx) ⇒ Object



84
85
86
87
88
89
90
91
92
93
# File 'lib/feat/client.rb', line 84

def evaluate(flag_key, default_value, ctx)
  df = @datafile
  if df.nil?
    return EvaluationResult.new(
      value: default_value, reason: Reason::ERROR,
      error_message: "client not ready: call #start before #evaluate"
    )
  end
  Eval.call(flag_key: flag_key, default_value: default_value, ctx: ctx, datafile: df)
end

#get_boolean_value(flag_key, default, ctx) ⇒ Object



95
96
97
98
# File 'lib/feat/client.rb', line 95

def get_boolean_value(flag_key, default, ctx)
  r = evaluate(flag_key, default, ctx)
  r.value == true || r.value == false ? r.value : default
end

#get_number_value(flag_key, default, ctx) ⇒ Object



105
106
107
108
# File 'lib/feat/client.rb', line 105

def get_number_value(flag_key, default, ctx)
  r = evaluate(flag_key, default, ctx)
  r.value.is_a?(Numeric) && !(r.value == true || r.value == false) ? r.value : default
end

#get_object_value(flag_key, default, ctx) ⇒ Object



110
111
112
113
# File 'lib/feat/client.rb', line 110

def get_object_value(flag_key, default, ctx)
  r = evaluate(flag_key, default, ctx)
  r.value
end

#get_string_value(flag_key, default, ctx) ⇒ Object



100
101
102
103
# File 'lib/feat/client.rb', line 100

def get_string_value(flag_key, default, ctx)
  r = evaluate(flag_key, default, ctx)
  r.value.is_a?(String) ? r.value : default
end

#refreshObject



80
81
82
# File 'lib/feat/client.rb', line 80

def refresh
  fetch_once
end

#startObject

Blocking initial fetch; spawns the background poller (and stream).



63
64
65
66
67
68
# File 'lib/feat/client.rb', line 63

def start
  refresh
  @streaming&.start
  @thread ||= Thread.new { poll_loop }
  self
end