/
toggle_fetcher.rb
executable file
·129 lines (109 loc) · 3.99 KB
/
toggle_fetcher.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
require 'unleash/configuration'
require 'net/http'
require 'json'
module Unleash
class ToggleFetcher
attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count
def initialize
self.etag = nil
self.toggle_cache = nil
self.toggle_lock = Mutex.new
self.toggle_resource = ConditionVariable.new
self.retry_count = 0
# start by fetching synchronously, and failing back to reading the backup file.
begin
fetch
rescue StandardError => e
Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
Unleash.logger.debug "Exception Caught: #{e}"
read!
end
# once initialized, somewhere else you will want to start a loop with fetch()
end
def toggles
self.toggle_lock.synchronize do
# wait for resource, only if it is null
self.toggle_resource.wait(self.toggle_lock) if self.toggle_cache.nil?
return self.toggle_cache
end
end
# rename to refresh_from_server! ??
def fetch
Unleash.logger.debug "fetch()"
response = Unleash::Util::Http.get(Unleash.configuration.fetch_toggles_uri, etag)
if response.code == '304'
Unleash.logger.debug "No changes according to the unleash server, nothing to do."
return
elsif response.code != '200'
raise IOError, "Unleash server returned a non 200/304 HTTP result."
end
self.etag = response['ETag']
response_hash = JSON.parse(response.body)
if response_hash['version'] >= 1
features = response_hash['features']
else
raise NotImplemented, "Version of features provided by unleash server" \
" is unsupported by this client."
end
# always synchronize with the local cache when fetching:
synchronize_with_local_cache!(features)
update_running_client!
save!
end
def save!
Unleash.logger.debug "Will save toggles to disk now"
begin
backup_file = Unleash.configuration.backup_file
backup_file_tmp = "#{backup_file}.tmp"
self.toggle_lock.synchronize do
file = File.open(backup_file_tmp, "w")
file.write(self.toggle_cache.to_json)
file.close
File.rename(backup_file_tmp, backup_file)
end
rescue StandardError => e
# This is not really the end of the world. Swallowing the exception.
Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
Unleash.logger.error "stacktrace: #{e.backtrace}"
ensure
file&.close if defined?(file)
self.toggle_lock.unlock if self.toggle_lock.locked?
end
end
private
def synchronize_with_local_cache!(features)
if self.toggle_cache != features
self.toggle_lock.synchronize do
self.toggle_cache = features
end
# notify all threads waiting for this resource to no longer wait
self.toggle_resource.broadcast
end
end
def update_running_client!
if Unleash.toggles != self.toggles
Unleash.logger.info "Updating toggles to main client, there has been a change in the server."
Unleash.toggles = self.toggles
end
end
def read!
Unleash.logger.debug "read!()"
return nil unless File.exist?(Unleash.configuration.backup_file)
begin
file = File.new(Unleash.configuration.backup_file, "r")
file_content = file.read
backup_as_hash = JSON.parse(file_content)
synchronize_with_local_cache!(backup_as_hash)
update_running_client!
rescue IOError => e
Unleash.logger.error "Unable to read the backup_file: #{e}"
rescue JSON::ParserError => e
Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
rescue StandardError => e
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
ensure
file&.close
end
end
end
end