super_settings 2.5.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +8 -7
- data/VERSION +1 -1
- data/app/helpers/super_settings/settings_helper.rb +1 -0
- data/lib/super_settings/application/helper.rb +9 -1
- data/lib/super_settings/application/layout.html.erb +56 -0
- data/lib/super_settings/application/layout_styles.css +37 -0
- data/lib/super_settings/application/layout_vars.css.erb +5 -1
- data/lib/super_settings/application/scripts.js +9 -4
- data/lib/super_settings/application/style_vars.css.erb +4 -2
- data/lib/super_settings/application.rb +10 -15
- data/lib/super_settings/coerce.rb +4 -7
- data/lib/super_settings/configuration.rb +36 -7
- data/lib/super_settings/controller_actions.rb +13 -3
- data/lib/super_settings/engine.rb +2 -2
- data/lib/super_settings/http_client.rb +23 -22
- data/lib/super_settings/local_cache.rb +8 -21
- data/lib/super_settings/mini_i18n.rb +9 -8
- data/lib/super_settings/rack_application.rb +52 -12
- data/lib/super_settings/rest_api.rb +9 -3
- data/lib/super_settings/setting.rb +35 -9
- data/lib/super_settings/storage/active_record_storage.rb +29 -14
- data/lib/super_settings/storage/json_storage.rb +49 -36
- data/lib/super_settings/storage/mongodb_storage.rb +11 -4
- data/lib/super_settings/storage/redis_storage.rb +1 -1
- data/lib/super_settings/storage/s3_storage.rb +25 -10
- data/lib/super_settings/storage/test_storage.rb +1 -0
- data/lib/super_settings/storage/transaction.rb +6 -4
- data/lib/super_settings.rb +1 -0
- data/super_settings.gemspec +0 -2
- metadata +2 -16
|
@@ -12,10 +12,6 @@ module SuperSettings
|
|
|
12
12
|
NOT_DEFINED = Object.new.freeze
|
|
13
13
|
private_constant :NOT_DEFINED
|
|
14
14
|
|
|
15
|
-
# @private
|
|
16
|
-
DRIFT_FACTOR = 10
|
|
17
|
-
private_constant :DRIFT_FACTOR
|
|
18
|
-
|
|
19
15
|
# Number of seconds that the cache will be considered fresh. The database will only be
|
|
20
16
|
# checked for changed settings at most this often.
|
|
21
17
|
attr_reader :refresh_interval
|
|
@@ -47,13 +43,15 @@ module SuperSettings
|
|
|
47
43
|
value = NOT_DEFINED
|
|
48
44
|
else
|
|
49
45
|
setting = Setting.find_by_key(key)
|
|
50
|
-
value = (setting ? setting.value : NOT_DEFINED)
|
|
46
|
+
value = (setting ? setting.value.freeze : NOT_DEFINED)
|
|
51
47
|
# Guard against caching too many cache missees; at some point it's better to slam
|
|
52
48
|
# the database rather than run out of memory.
|
|
53
49
|
if setting || @cache.size < 100_000
|
|
54
50
|
@lock.synchronize do
|
|
55
|
-
#
|
|
56
|
-
|
|
51
|
+
# Don't overwrite an entry added by a concurrent refresh; it is at least as
|
|
52
|
+
# fresh as the value read here. A new hash is set so that one thread can
|
|
53
|
+
# iterate over the cache while it's updated without causing an error.
|
|
54
|
+
@cache = @cache.merge(key => value).freeze unless @cache.include?(key)
|
|
57
55
|
end
|
|
58
56
|
end
|
|
59
57
|
end
|
|
@@ -91,8 +89,7 @@ module SuperSettings
|
|
|
91
89
|
def to_h
|
|
92
90
|
ensure_cache_up_to_date!
|
|
93
91
|
hash = {}
|
|
94
|
-
@cache.each do |key,
|
|
95
|
-
value, _ = data
|
|
92
|
+
@cache.each do |key, value|
|
|
96
93
|
hash[key] = value unless value == NOT_DEFINED
|
|
97
94
|
end
|
|
98
95
|
hash
|
|
@@ -150,8 +147,6 @@ module SuperSettings
|
|
|
150
147
|
return if @refreshing
|
|
151
148
|
|
|
152
149
|
@next_check_at = Time.now + @refresh_interval
|
|
153
|
-
return if @cache.empty?
|
|
154
|
-
|
|
155
150
|
@refreshing = true
|
|
156
151
|
end
|
|
157
152
|
|
|
@@ -199,7 +194,7 @@ module SuperSettings
|
|
|
199
194
|
return if Coerce.blank?(setting.key)
|
|
200
195
|
|
|
201
196
|
@lock.synchronize do
|
|
202
|
-
@cache = @cache.merge(setting.key => setting.value)
|
|
197
|
+
@cache = @cache.merge(setting.key => setting.value.freeze).freeze
|
|
203
198
|
end
|
|
204
199
|
end
|
|
205
200
|
|
|
@@ -220,7 +215,7 @@ module SuperSettings
|
|
|
220
215
|
changed_settings = {}
|
|
221
216
|
start_time = Time.now
|
|
222
217
|
Setting.updated_since(last_refresh_time - 1).each do |setting|
|
|
223
|
-
value = (setting.deleted? ? NOT_DEFINED : setting.value)
|
|
218
|
+
value = (setting.deleted? ? NOT_DEFINED : setting.value.freeze)
|
|
224
219
|
changed_settings[setting.key] = value
|
|
225
220
|
end
|
|
226
221
|
set_cache_values(start_time) { @cache.merge(changed_settings) }
|
|
@@ -249,13 +244,5 @@ module SuperSettings
|
|
|
249
244
|
@cache = block.call.freeze
|
|
250
245
|
end
|
|
251
246
|
end
|
|
252
|
-
|
|
253
|
-
# Recursively freeze a hash.
|
|
254
|
-
def deep_freeze_hash(hash)
|
|
255
|
-
hash.each_value do |value|
|
|
256
|
-
deep_freeze_hash(value) if value.is_a?(Hash)
|
|
257
|
-
end
|
|
258
|
-
hash.freeze
|
|
259
|
-
end
|
|
260
247
|
end
|
|
261
248
|
end
|
|
@@ -78,21 +78,21 @@ module SuperSettings
|
|
|
78
78
|
# Load every JSON file from the locales directory, keyed by filename
|
|
79
79
|
# stem (e.g. "en").
|
|
80
80
|
def load_all_locales
|
|
81
|
-
|
|
82
|
-
@mutex.synchronize { @cache = {} }
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
return @cache unless @cache.empty?
|
|
81
|
+
return @cache unless @cache.empty? || development_mode?
|
|
86
82
|
|
|
87
83
|
@mutex.synchronize do
|
|
88
|
-
return @cache unless @cache.empty?
|
|
84
|
+
return @cache unless @cache.empty? || development_mode?
|
|
89
85
|
|
|
86
|
+
# Build into a new hash so that other threads never see a partially
|
|
87
|
+
# loaded cache.
|
|
88
|
+
cache = {}
|
|
90
89
|
Dir.glob(File.join(locales_dir, "*.json")).each do |path|
|
|
91
90
|
code = File.basename(path, ".json").downcase
|
|
92
|
-
|
|
91
|
+
cache[code] = JSON.parse(File.read(path))
|
|
93
92
|
rescue JSON::ParserError
|
|
94
93
|
# Skip malformed locale files
|
|
95
94
|
end
|
|
95
|
+
@cache = cache
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
@cache
|
|
@@ -103,7 +103,8 @@ module SuperSettings
|
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def development_mode?
|
|
106
|
-
ENV
|
|
106
|
+
env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["APP_ENV"] || "development"
|
|
107
|
+
env == "development"
|
|
107
108
|
end
|
|
108
109
|
end
|
|
109
110
|
end
|
|
@@ -52,8 +52,10 @@ module SuperSettings
|
|
|
52
52
|
def call(env)
|
|
53
53
|
if @path_prefix.empty? || "#{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}".start_with?(@path_prefix)
|
|
54
54
|
handle_request(env)
|
|
55
|
-
|
|
55
|
+
elsif @app
|
|
56
56
|
@app.call(env)
|
|
57
|
+
else
|
|
58
|
+
[404, {"content-type" => "text/plain"}, ["Not found"]]
|
|
57
59
|
end
|
|
58
60
|
end
|
|
59
61
|
|
|
@@ -104,9 +106,9 @@ module SuperSettings
|
|
|
104
106
|
# Subclasses can override this method to return the path to an ERB file to use as the layout
|
|
105
107
|
# for the HTML application. The layout can use any of the methods defined in SuperSettings::Application::Helper.
|
|
106
108
|
#
|
|
107
|
-
# @return [String]
|
|
109
|
+
# @return [String, Symbol]
|
|
108
110
|
def layout
|
|
109
|
-
|
|
111
|
+
:default
|
|
110
112
|
end
|
|
111
113
|
|
|
112
114
|
# Subclasses can override this method to add custom HTML to the <head> element in the HTML application.
|
|
@@ -129,7 +131,16 @@ module SuperSettings
|
|
|
129
131
|
|
|
130
132
|
def handle_request(env)
|
|
131
133
|
request = Rack::Request.new(env)
|
|
132
|
-
|
|
134
|
+
script_name = env["SCRIPT_NAME"].to_s
|
|
135
|
+
full_path = "#{script_name}#{env["PATH_INFO"]}"
|
|
136
|
+
path = if !@path_prefix.empty? && full_path.start_with?(@path_prefix)
|
|
137
|
+
full_path[@path_prefix.length, full_path.length]
|
|
138
|
+
elsif @path_prefix.empty? && !script_name.empty?
|
|
139
|
+
# The application is mounted at SCRIPT_NAME, which the router has already consumed.
|
|
140
|
+
env["PATH_INFO"].to_s
|
|
141
|
+
else
|
|
142
|
+
full_path
|
|
143
|
+
end
|
|
133
144
|
if request.get?
|
|
134
145
|
if (path == "/" || path == "") && web_ui_enabled?
|
|
135
146
|
return handle_root_request(request)
|
|
@@ -186,13 +197,21 @@ module SuperSettings
|
|
|
186
197
|
if lang && SuperSettings::MiniI18n.available_locales.include?(lang)
|
|
187
198
|
headers["set-cookie"] = "super_settings_locale=#{lang}; path=/; SameSite=Lax"
|
|
188
199
|
end
|
|
189
|
-
|
|
200
|
+
application = Application.new(
|
|
201
|
+
layout: layout,
|
|
202
|
+
add_to_head: add_to_head(request),
|
|
203
|
+
color_scheme: SuperSettings.configuration.controller.color_scheme,
|
|
204
|
+
dark_mode_selector: SuperSettings.configuration.controller.resolved_dark_mode_selector,
|
|
205
|
+
read_only: read_only,
|
|
206
|
+
locale: locale
|
|
207
|
+
)
|
|
208
|
+
[200, headers, [application.render]]
|
|
190
209
|
end
|
|
191
210
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
211
|
+
# Only unauthenticated requests are redirected to the login page. An authenticated
|
|
212
|
+
# user who is denied access would just be bounced back here in a redirect loop.
|
|
213
|
+
if response.first == 401 && SuperSettings.authentication_url
|
|
214
|
+
response = [302, {"location" => SuperSettings.authentication_url}, []]
|
|
196
215
|
end
|
|
197
216
|
|
|
198
217
|
response
|
|
@@ -217,7 +236,11 @@ module SuperSettings
|
|
|
217
236
|
|
|
218
237
|
def handle_update_request(request)
|
|
219
238
|
check_authorization(request, write_required: true) do |user|
|
|
220
|
-
|
|
239
|
+
params = post_params(request)
|
|
240
|
+
settings = params["settings"] if params
|
|
241
|
+
next json_response(400, error: "Invalid request") unless valid_settings_params?(settings)
|
|
242
|
+
|
|
243
|
+
result = SuperSettings::RestAPI.update(settings, changed_by(user))
|
|
221
244
|
if result[:success]
|
|
222
245
|
json_response(200, result)
|
|
223
246
|
else
|
|
@@ -245,7 +268,12 @@ module SuperSettings
|
|
|
245
268
|
|
|
246
269
|
def handle_updated_since_request(request)
|
|
247
270
|
check_authorization(request) do |user|
|
|
248
|
-
|
|
271
|
+
result = RestAPI.updated_since(request.params["time"])
|
|
272
|
+
if result
|
|
273
|
+
json_response(200, result)
|
|
274
|
+
else
|
|
275
|
+
json_response(400, error: "Invalid time parameter")
|
|
276
|
+
end
|
|
249
277
|
end
|
|
250
278
|
end
|
|
251
279
|
|
|
@@ -315,12 +343,24 @@ module SuperSettings
|
|
|
315
343
|
nil
|
|
316
344
|
end
|
|
317
345
|
|
|
346
|
+
# Returns the request parameters merged with any JSON request body. Returns nil
|
|
347
|
+
# if the request body is not valid JSON.
|
|
318
348
|
def post_params(request)
|
|
319
349
|
if request.content_type.to_s.match?(/\Aapplication\/json/i) && request.body
|
|
320
|
-
|
|
350
|
+
body = JSON.parse(request.body.read)
|
|
351
|
+
return nil unless body.is_a?(Hash)
|
|
352
|
+
|
|
353
|
+
request.params.merge(body)
|
|
321
354
|
else
|
|
322
355
|
request.params
|
|
323
356
|
end
|
|
357
|
+
rescue JSON::ParserError
|
|
358
|
+
nil
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# The settings parameter in an update request must be an array of hashes.
|
|
362
|
+
def valid_settings_params?(settings)
|
|
363
|
+
settings.is_a?(Array) && settings.all? { |setting| setting.is_a?(Hash) }
|
|
324
364
|
end
|
|
325
365
|
end
|
|
326
366
|
end
|
|
@@ -163,7 +163,7 @@ module SuperSettings
|
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
payload[:histories] = histories.collect do |history|
|
|
166
|
-
history_values = {value: history.value, changed_by: history.changed_by_display, created_at: history.created_at
|
|
166
|
+
history_values = {value: history.value, changed_by: history.changed_by_display, created_at: history.created_at&.utc&.iso8601(6)}
|
|
167
167
|
history_values[:deleted] = true if history.deleted?
|
|
168
168
|
history_values
|
|
169
169
|
end
|
|
@@ -209,9 +209,15 @@ module SuperSettings
|
|
|
209
209
|
# ...
|
|
210
210
|
# ]
|
|
211
211
|
#
|
|
212
|
-
# @return [Hash] hash with settings array
|
|
212
|
+
# @return [Hash, nil] hash with settings array or nil if the time is missing or unparseable
|
|
213
213
|
def updated_since(time)
|
|
214
|
-
time =
|
|
214
|
+
time = begin
|
|
215
|
+
Coerce.time(time)
|
|
216
|
+
rescue ArgumentError
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
return nil if time.nil?
|
|
220
|
+
|
|
215
221
|
settings = Setting.updated_since(time).reject(&:deleted?)
|
|
216
222
|
{settings: settings.collect(&:as_json)}
|
|
217
223
|
end
|
|
@@ -30,6 +30,10 @@ module SuperSettings
|
|
|
30
30
|
class InvalidRecordError < StandardError
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Exception raised if the storage engine could not persist valid changes.
|
|
34
|
+
class PersistenceError < StandardError
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
include Attributes
|
|
34
38
|
|
|
35
39
|
# The changed_by attribute is used to temporarily store an identifier for the user
|
|
@@ -164,12 +168,23 @@ module SuperSettings
|
|
|
164
168
|
def bulk_update(params, changed_by = nil)
|
|
165
169
|
all_valid, settings = update_settings(params, changed_by)
|
|
166
170
|
if all_valid
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
setting
|
|
171
|
+
begin
|
|
172
|
+
storage.with_connection do
|
|
173
|
+
transaction do |_changes|
|
|
174
|
+
settings.each do |setting|
|
|
175
|
+
setting.save!
|
|
176
|
+
end
|
|
171
177
|
end
|
|
172
178
|
end
|
|
179
|
+
rescue InvalidRecordError, PersistenceError => e
|
|
180
|
+
# Validation failures detected by save! are already recorded on the setting that
|
|
181
|
+
# failed. A storage level failure isn't attributable to any single setting, so
|
|
182
|
+
# record it on all of them; otherwise callers would get a failure with no
|
|
183
|
+
# explanation of what went wrong.
|
|
184
|
+
if settings.none? { |setting| setting.errors.any? }
|
|
185
|
+
settings.each { |setting| setting.send(:add_base_error, e.message) }
|
|
186
|
+
end
|
|
187
|
+
return [false, settings]
|
|
173
188
|
end
|
|
174
189
|
clear_last_updated_cache
|
|
175
190
|
end
|
|
@@ -214,7 +229,7 @@ module SuperSettings
|
|
|
214
229
|
Thread.current[:super_settings_transaction] = changes
|
|
215
230
|
|
|
216
231
|
begin
|
|
217
|
-
|
|
232
|
+
storage.transaction(&block)
|
|
218
233
|
|
|
219
234
|
clear_last_updated_cache
|
|
220
235
|
|
|
@@ -382,7 +397,7 @@ module SuperSettings
|
|
|
382
397
|
# @param val [String]
|
|
383
398
|
def description=(val)
|
|
384
399
|
val = val&.to_s
|
|
385
|
-
val = nil if val
|
|
400
|
+
val = nil if val && val.empty?
|
|
386
401
|
will_change!(:description, val) unless description == val
|
|
387
402
|
@record.description = val
|
|
388
403
|
end
|
|
@@ -484,12 +499,12 @@ module SuperSettings
|
|
|
484
499
|
raise InvalidRecordError.new(errors.values.join("; "))
|
|
485
500
|
end
|
|
486
501
|
|
|
502
|
+
return if @changes.empty?
|
|
503
|
+
|
|
487
504
|
timestamp = Time.now
|
|
488
505
|
self.created_at ||= timestamp
|
|
489
506
|
self.updated_at = timestamp if updated_at.nil? || !changed?(:updated_at)
|
|
490
507
|
|
|
491
|
-
return if @changes.empty?
|
|
492
|
-
|
|
493
508
|
self.class.storage.with_connection do
|
|
494
509
|
self.class.transaction do
|
|
495
510
|
record_value_change
|
|
@@ -664,7 +679,7 @@ module SuperSettings
|
|
|
664
679
|
|
|
665
680
|
def raw_value=(val)
|
|
666
681
|
val = val&.to_s
|
|
667
|
-
val = nil if val
|
|
682
|
+
val = nil if val && val.empty?
|
|
668
683
|
will_change!(:raw_value, val) unless raw_value == val
|
|
669
684
|
@raw_value = val
|
|
670
685
|
@record.raw_value = val
|
|
@@ -706,6 +721,17 @@ module SuperSettings
|
|
|
706
721
|
attribute_errors << "#{attribute.tr("_", " ")} #{message}"
|
|
707
722
|
end
|
|
708
723
|
|
|
724
|
+
# Record an error that applies to the record as a whole rather than to one attribute.
|
|
725
|
+
# The message is used verbatim since there is no attribute name to prefix it with.
|
|
726
|
+
def add_base_error(message)
|
|
727
|
+
base_errors = @errors["base"]
|
|
728
|
+
unless base_errors
|
|
729
|
+
base_errors = []
|
|
730
|
+
@errors["base"] = base_errors
|
|
731
|
+
end
|
|
732
|
+
base_errors << message
|
|
733
|
+
end
|
|
734
|
+
|
|
709
735
|
def call_after_save_callbacks
|
|
710
736
|
self.class.after_save_blocks.each do |block|
|
|
711
737
|
block.call(self)
|
|
@@ -103,22 +103,37 @@ module SuperSettings
|
|
|
103
103
|
def save!
|
|
104
104
|
# Check if another record with the same key exists. If it does, then we need to update
|
|
105
105
|
# that record instead and delete the current one.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@model.
|
|
106
|
+
attempts = 0
|
|
107
|
+
begin
|
|
108
|
+
# Each attempt runs in a savepoint so a duplicate key failure can be rolled back
|
|
109
|
+
# and retried; otherwise the surrounding transaction would be left in an aborted
|
|
110
|
+
# state on PostgreSQL.
|
|
111
|
+
@model.class.transaction(requires_new: true) do
|
|
112
|
+
duplicate = @model.class.find_by(key: @model.key)
|
|
113
|
+
if duplicate.nil? || duplicate == @model
|
|
114
|
+
@model.save!
|
|
115
|
+
else
|
|
116
|
+
duplicate.raw_value = @model.raw_value
|
|
117
|
+
duplicate.value_type = @model.value_type
|
|
118
|
+
duplicate.description = @model.description
|
|
119
|
+
duplicate.deleted = @model.deleted
|
|
120
|
+
|
|
121
|
+
if @model.persisted?
|
|
122
|
+
begin
|
|
123
|
+
@model.reload.update!(deleted: true)
|
|
124
|
+
rescue ActiveRecord::RecordNotFound
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
duplicate.save!
|
|
128
|
+
@model = duplicate
|
|
118
129
|
end
|
|
119
|
-
duplicate.save!
|
|
120
130
|
end
|
|
121
|
-
|
|
131
|
+
rescue ActiveRecord::RecordNotUnique
|
|
132
|
+
# A record with the same key was inserted concurrently; retry so it is
|
|
133
|
+
# found as a duplicate and merged.
|
|
134
|
+
attempts += 1
|
|
135
|
+
retry if attempts <= 1
|
|
136
|
+
raise
|
|
122
137
|
end
|
|
123
138
|
end
|
|
124
139
|
|
|
@@ -11,6 +11,11 @@ module SuperSettings
|
|
|
11
11
|
# This class can be used as the base for any storage class where the settings are all stored
|
|
12
12
|
# together in a single JSON payload.
|
|
13
13
|
#
|
|
14
|
+
# Writes are serialized within a process, but there is no coordination between processes.
|
|
15
|
+
# If multiple processes write settings at the same time, the last write wins and can
|
|
16
|
+
# overwrite changes made by another process. Storage backends based on this class are best
|
|
17
|
+
# suited for setups where settings are updated from a single process at a time.
|
|
18
|
+
#
|
|
14
19
|
# Subclasses must implement the following methods:
|
|
15
20
|
# - self.all
|
|
16
21
|
# - self.last_updated_at
|
|
@@ -18,6 +23,12 @@ module SuperSettings
|
|
|
18
23
|
class JSONStorage < StorageAttributes
|
|
19
24
|
include Transaction
|
|
20
25
|
|
|
26
|
+
# Mutex used to serialize the read-modify-write cycle in save_all within a process.
|
|
27
|
+
# Note that this cannot protect against concurrent writes from multiple processes;
|
|
28
|
+
# in that situation the last writer wins and can overwrite another process's changes.
|
|
29
|
+
SAVE_MUTEX = Mutex.new
|
|
30
|
+
private_constant :SAVE_MUTEX
|
|
31
|
+
|
|
21
32
|
class HistoryStorage < HistoryAttributes
|
|
22
33
|
class << self
|
|
23
34
|
def create!(attributes)
|
|
@@ -65,50 +76,52 @@ module SuperSettings
|
|
|
65
76
|
end
|
|
66
77
|
|
|
67
78
|
def save_all(changes)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
SAVE_MUTEX.synchronize do
|
|
80
|
+
existing = {}
|
|
81
|
+
parse_settings(settings_json_payload).each do |setting|
|
|
82
|
+
existing[setting.key] = setting
|
|
83
|
+
end
|
|
72
84
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
history_items = []
|
|
86
|
+
changes.each do |record|
|
|
87
|
+
if record.is_a?(HistoryStorage)
|
|
88
|
+
history_items << record
|
|
89
|
+
else
|
|
90
|
+
existing[record.key] = record
|
|
91
|
+
end
|
|
79
92
|
end
|
|
80
|
-
end
|
|
81
93
|
|
|
82
|
-
|
|
94
|
+
settings = existing.values.sort_by(&:key)
|
|
83
95
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
changed_histories = {}
|
|
97
|
+
history_items.each do |history_item|
|
|
98
|
+
setting = existing[history_item.key]
|
|
99
|
+
next unless setting
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
history = changed_histories[history_item.key]
|
|
102
|
+
unless history
|
|
103
|
+
history = setting.history.dup
|
|
104
|
+
changed_histories[history_item.key] = history
|
|
105
|
+
end
|
|
106
|
+
history.unshift(history_item)
|
|
93
107
|
end
|
|
94
|
-
history.unshift(history_item)
|
|
95
|
-
end
|
|
96
108
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
settings_json = JSON.dump(settings.collect(&:as_json))
|
|
110
|
+
save_settings_json(settings_json)
|
|
111
|
+
|
|
112
|
+
changed_histories.each do |setting_key, setting_history|
|
|
113
|
+
ordered_history = setting_history.sort_by { |history_item| history_item.created_at }.reverse
|
|
114
|
+
payload = ordered_history.collect do |history_item|
|
|
115
|
+
{
|
|
116
|
+
value: history_item.value,
|
|
117
|
+
changed_by: history_item.changed_by,
|
|
118
|
+
created_at: history_item.created_at.iso8601(6),
|
|
119
|
+
deleted: history_item.deleted?
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
history_json = JSON.dump(payload)
|
|
123
|
+
save_history_json(setting_key, history_json)
|
|
109
124
|
end
|
|
110
|
-
history_json = JSON.dump(payload)
|
|
111
|
-
save_history_json(setting_key, history_json)
|
|
112
125
|
end
|
|
113
126
|
end
|
|
114
127
|
|
|
@@ -65,9 +65,12 @@ module SuperSettings
|
|
|
65
65
|
if @mongodb.nil? || @url_hash != @url.hash
|
|
66
66
|
@mutex.synchronize do
|
|
67
67
|
unless @url_hash == @url.hash
|
|
68
|
+
client = Mongo::Client.new(@url)
|
|
69
|
+
create_indexes!(client)
|
|
70
|
+
# Only record the URL hash after the client is set up so that a
|
|
71
|
+
# failure here will be retried on the next call.
|
|
72
|
+
@mongodb = client
|
|
68
73
|
@url_hash = @url.hash
|
|
69
|
-
@mongodb = Mongo::Client.new(@url)
|
|
70
|
-
create_indexes!(@mongodb)
|
|
71
74
|
end
|
|
72
75
|
end
|
|
73
76
|
end
|
|
@@ -100,8 +103,12 @@ module SuperSettings
|
|
|
100
103
|
key: key,
|
|
101
104
|
deleted: false
|
|
102
105
|
}
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
attributes = settings_collection.find(query).projection(history: 0).first
|
|
107
|
+
if attributes
|
|
108
|
+
record = new(attributes)
|
|
109
|
+
record.persisted = true
|
|
110
|
+
record
|
|
111
|
+
end
|
|
105
112
|
end
|
|
106
113
|
|
|
107
114
|
def last_updated_at
|
|
@@ -123,7 +123,7 @@ module SuperSettings
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
def last_updated_at
|
|
126
|
-
result = with_redis { |redis| redis.zrevrange(UPDATED_KEY, 0,
|
|
126
|
+
result = with_redis { |redis| redis.zrevrange(UPDATED_KEY, 0, 0, withscores: true).first }
|
|
127
127
|
return nil unless result
|
|
128
128
|
|
|
129
129
|
time_at_microseconds(result[1])
|
|
@@ -69,6 +69,9 @@ module SuperSettings
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
BUCKET_MUTEX = Mutex.new
|
|
73
|
+
private_constant :BUCKET_MUTEX
|
|
74
|
+
|
|
72
75
|
@bucket = nil
|
|
73
76
|
@bucket_hash = nil
|
|
74
77
|
|
|
@@ -115,20 +118,32 @@ module SuperSettings
|
|
|
115
118
|
private
|
|
116
119
|
|
|
117
120
|
def s3_bucket
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
# The mutex ensures the bucket and the configuration hash it was built from are
|
|
122
|
+
# always published together. The bucket and the hash are derived from a single
|
|
123
|
+
# read of the configuration attributes so that a concurrent configuration change
|
|
124
|
+
# cannot cache a bucket built from mixed values under the settled configuration
|
|
125
|
+
# hash; a torn read produces a hash that will not match the settled configuration,
|
|
126
|
+
# so the bucket gets rebuilt on the next call.
|
|
127
|
+
BUCKET_MUTEX.synchronize do
|
|
128
|
+
config = configuration
|
|
120
129
|
options = {
|
|
121
|
-
endpoint:
|
|
122
|
-
access_key_id:
|
|
123
|
-
secret_access_key:
|
|
124
|
-
region:
|
|
130
|
+
endpoint: config.endpoint,
|
|
131
|
+
access_key_id: config.access_key_id,
|
|
132
|
+
secret_access_key: config.secret_access_key,
|
|
133
|
+
region: config.region
|
|
125
134
|
}
|
|
126
|
-
|
|
127
|
-
options.
|
|
135
|
+
bucket_name = config.bucket
|
|
136
|
+
config_hash = [options, bucket_name, config.path].hash
|
|
137
|
+
|
|
138
|
+
if config_hash != @bucket_hash
|
|
139
|
+
options[:force_path_style] = true if options[:endpoint]
|
|
140
|
+
options.compact!
|
|
128
141
|
|
|
129
|
-
|
|
142
|
+
@bucket = Aws::S3::Resource.new(options).bucket(bucket_name)
|
|
143
|
+
@bucket_hash = config_hash
|
|
144
|
+
end
|
|
145
|
+
@bucket
|
|
130
146
|
end
|
|
131
|
-
@bucket
|
|
132
147
|
end
|
|
133
148
|
|
|
134
149
|
def s3_object(filename)
|
|
@@ -20,10 +20,12 @@ module SuperSettings
|
|
|
20
20
|
|
|
21
21
|
yield(changes)
|
|
22
22
|
|
|
23
|
-
if save_all(changes)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
if save_all(changes) == false
|
|
24
|
+
raise SuperSettings::Setting::PersistenceError.new("Settings could not be saved")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
changes.each do |object|
|
|
28
|
+
object.persisted = true if object.respond_to?(:persisted=)
|
|
27
29
|
end
|
|
28
30
|
ensure
|
|
29
31
|
Thread.current[transaction_key] = nil
|
data/lib/super_settings.rb
CHANGED