Class: DevCycle::ConfigManager

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb

Instance Method Summary collapse

Constructor Details

#initialize(sdkKey, local_bucketing, wait_for_init) ⇒ ConfigManager

Returns a new instance of ConfigManager.



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 18

def initialize(sdkKey, local_bucketing, wait_for_init)
  @first_load = true
  @config_version = "v2"
  @local_bucketing = local_bucketing
  @sdkKey = sdkKey
  @sse_url = ""
  @sse_polling = false
  @config_e_tag = ""
  @config_last_modified = ""
  @logger = local_bucketing.options.logger
  @enable_sse = local_bucketing.options.enable_beta_realtime_updates
  @polling_enabled = true
  @sse_active = false
  @max_config_retries = 2
  @config_poller = Concurrent::TimerTask.new({
                                               execution_interval: @local_bucketing.options.config_polling_interval_ms.fdiv(1000)
                                             }) do |_|
    fetch_config
  end

  t = Thread.new { initialize_config }
  t.join if wait_for_init
end

Instance Method Details

#closeObject



186
187
188
189
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 186

def close
  @config_poller.shutdown if @config_poller.running?
  nil
end

#fetch_config(min_last_modified: -1)) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 53

def fetch_config(min_last_modified: -1)
  return unless @polling_enabled || (@sse_active && @enable_sse)

  req = Typhoeus::Request.new(
    get_config_url,
    headers: {
      Accept: "application/json",
    })

  begin
    # Blind parse the lastmodified string to check if it's a valid date.
    # This short circuits the rest of the checks if it's not set
    if @config_last_modified != ""
      if min_last_modified != -1
        stored_date = Date.parse(@config_last_modified)
        parsed_sse_ts = Time.at(min_last_modified)
        if parsed_sse_ts.utc > stored_date.utc
          req.options[:headers]["If-Modified-Since"] = parsed_sse_ts.utc.httpdate
        else
          req.options[:headers]["If-Modified-Since"] = @config_last_modified
        end
      else
        req.options[:headers]["If-Modified-Since"] = @config_last_modified
      end
    end
  rescue
  end

  if @config_e_tag != ""
    req.options[:headers]['If-None-Match'] = @config_e_tag
  end

  @max_config_retries.times do
    @logger.debug("Requesting new config from #{get_config_url}, current etag: #{@config_e_tag}, last modified: #{@config_last_modified}")
    resp = req.run
    @logger.debug("Config request complete, status: #{resp.code}")
    case resp.code
    when 304
      @logger.debug("Config not modified, using cache, etag: #{@config_e_tag}, last modified: #{@config_last_modified}")
      break
    when 200
      @logger.debug("New config received, etag: #{resp.headers['Etag']} LastModified:#{resp.headers['Last-Modified']}")
      begin
        if @config_last_modified == ""
          set_config(resp.body, resp.headers['Etag'], resp.headers['Last-Modified'])
          return
        end

        lm_timestamp = Time.rfc2822(resp.headers['Last-Modified'])
        current_lm = Time.rfc2822(@config_last_modified)
        if lm_timestamp == "" && @config_last_modified == "" || (current_lm.utc < lm_timestamp.utc)
          set_config(resp.body, resp.headers['Etag'], resp.headers['Last-Modified'])
        else
          @logger.warn("Config last-modified was an older date than currently stored config.")
        end
      rescue
        @logger.warn("Failed to parse last modified header, setting config anyway.")
        set_config(resp.body, resp.headers['Etag'], resp.headers['Last-Modified'])
      end
      break
    when 403
      stop_polling
      stop_sse
      @logger.error("Failed to download DevCycle config; Invalid SDK Key.")
      break
    when 404
      stop_polling
      stop_sse
      @logger.error("Failed to download DevCycle config; Config not found.")
      break
    when 500...599
      @logger.error("Failed to download DevCycle config. Status: #{resp.code}")
    else
      @logger.error("Unexpected response from DevCycle CDN")
      @logger.error("Response code: #{resp.code}")
      @logger.error("Response body: #{resp.body}")
      break
    end
  end
  nil
end

#get_config_urlObject



156
157
158
159
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 156

def get_config_url
  configBasePath = @local_bucketing.options.config_cdn_uri
  "#{configBasePath}/config/#{@config_version}/server/#{@sdkKey}.json"
end

#handle_sse(event_data) ⇒ Object



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 207

def handle_sse(event_data)
  unless @sse_polling
    stop_polling
    start_polling(true)
  end
  if event_data["data"] == nil
    return
  end
  @logger.debug("SSE: Message received: #{event_data["data"]}")
  parsed_event_data = JSON.parse(event_data["data"])

  last_modified = parsed_event_data["lastModified"]
  event_type = parsed_event_data["type"]

  if event_type == "refetchConfig" || event_type == nil
    @logger.debug("SSE: Re-fetching new config with TS: #{last_modified}")
    fetch_config(min_last_modified: last_modified / 1000)
  end
end

#init_sse(path) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 191

def init_sse(path)
  return unless @enable_sse
  @logger.debug("Initializing SSE with url: #{path}")
  @sse_active = true
  @sse_client = SSE::Client.new(path) do |client|
    client.on_event do |event|

      parsed_json = JSON.parse(event.data)
      handle_sse(parsed_json)
    end
    client.on_error do |error|
      @logger.debug("SSE Error: #{error.message}")
    end
  end
end

#initialize_configObject



42
43
44
45
46
47
48
49
50
51
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 42

def initialize_config
  begin
    fetch_config
    start_polling(false)
  rescue => e
    @logger.error("DevCycle: Error Initializing Config: #{e.message}")
  ensure
    @local_bucketing.initialized = true
  end
end

#set_config(config, etag, lastmodified) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 135

def set_config(config, etag, lastmodified)
  if !JSON.parse(config).is_a?(Hash)
    raise("Invalid JSON body parsed from Config Response")
  end
  parsed_config = JSON.parse(config)

  if parsed_config['sse'] != nil
    raw_url = "#{parsed_config['sse']['hostname']}#{parsed_config['sse']['path']}"
    if @sse_url != raw_url && raw_url != ""
      stop_sse
      @sse_url = raw_url
      init_sse(@sse_url)
    end
  end
  @local_bucketing.store_config(config)
  @config_e_tag = etag
  @config_last_modified = lastmodified
  @local_bucketing.has_config = true
  @logger.debug("New config stored, etag: #{@config_e_tag}, last modified: #{@config_last_modified}")
end

#start_polling(sse) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 161

def start_polling(sse)
  if sse
    @config_poller.shutdown if @config_poller.running?
    @config_poller = Concurrent::TimerTask.new({ execution_interval: 60 * 10 }) do |_|
      fetch_config
    end
    @sse_polling = sse
  end
  @polling_enabled = true
  @config_poller.execute if @polling_enabled && (!@sse_active || sse)
end

#stop_pollingObject



173
174
175
176
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 173

def stop_polling()
  @polling_enabled = false
  @config_poller.shutdown if @config_poller.running?
end

#stop_sseObject



178
179
180
181
182
183
184
# File 'lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb', line 178

def stop_sse
  return unless @enable_sse
  @sse_active = false
  @sse_polling = false
  @sse_client.close if @sse_client
  start_polling(@sse_polling)
end