process_settings 0.4.0.pre.11 → 0.4.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae29f9011f41199d20fb3ea7d752808f073747d3
4
- data.tar.gz: f223b8d3815773cd89bf77d586fba51ffbfcf4cb
3
+ metadata.gz: 191a509df72fd317ad7475301c1c1f9ed2b9234d
4
+ data.tar.gz: 32913ebed29db8091d71c1e669d75964f0b2170f
5
5
  SHA512:
6
- metadata.gz: fdb5acfd1b2729d0973d48cf59af213bb37fe6551a2acc22a20c4bbbfb465b9856e0309306d1e0797c0b475ba3c6274acf53e485820cc7ce7f43f369ba4c0878
7
- data.tar.gz: 7b7cfd35fff4fc5beadd5dfe57c73fb572ef80e2c350027162cf9c668d7482ed59318eb318e359c39cba08f2f94a6aa6368a9e432f5869786caf4b7b535cf87a
6
+ metadata.gz: b09368b7c8d4d091e955d492be4231cc909719e544d170484e79f1ee27af606c42e927e87f465e7cb7587b02ecbe7750a0ff2bee4f2d91a75dd4dfd8cfc6f327
7
+ data.tar.gz: 7aa5af5281aec6c0c9ec14f9c47c9bdf5e276ba7d6ceeb348dc2e62eebddb5f0553eeb13bcc6271c0a3ef7c957f6faaa6fbee27515ce007eed79c0ec5cf306bc
data/README.md CHANGED
@@ -1,9 +1,7 @@
1
- # ProcessSettings
2
- This gem provides dynamic settings for Ruby processes. The settings are stored in YAML.
3
- Settings are managed in a git repo, in separate YAML files for each concern (for example, each micro-service). Each YAML file can be targeted based on matching context values (for example, `service_name`).
4
-
5
-
6
- The context can be either static to the process (for example, `service_name` or `data_center`) or dynamic (for example, the current web request `domain`).
1
+ # ProcessSettings [![Build Status](https://travis-ci.org/Invoca/process_settings.svg?branch=master)](https://travis-ci.org/Invoca/process_settings) [![Coverage Status](https://coveralls.io/repos/github/Invoca/process_settings/badge.svg?branch=master)](https://coveralls.io/github/Invoca/process_settings?branch=master) [![Gem Version](https://badge.fury.io/rb/process_settings.svg)](https://badge.fury.io/rb/process_settings)
2
+ This gem provides dynamic settings for Linux processes. These settings are stored in JSON.
3
+ They including a targeting notation so that each settings group can be targeted based on matching context values.
4
+ The context can be either static to the process (for example, service_name or data_center) or dynamic (for example, the domain of the current web request).
7
5
 
8
6
  ## Installation
9
7
  To install this gem directly on your machine from rubygems, run the following:
@@ -13,151 +11,27 @@ gem install process_settings
13
11
 
14
12
  To install this gem in your bundler project, add the following to your Gemfile:
15
13
  ```ruby
16
- gem 'process_settings', '~> 0.4'
14
+ gem 'process_settings', '~> 0.3'
17
15
  ```
18
16
 
19
- ## Usage
20
- The `ProcessSettings::Monitor` and related classes can be freely created and used at any time.
21
- But typical usage is through the `ProcessSettings::Monitor.instance`.
22
- That should be configured at process startup before use.
23
- ### Configuration
24
- Before using `ProcessSettings::Monitor.instance`, you must first configure the path to the combined process settings file on disk,
25
- and provide a logger.
17
+ To use an unreleased version, add it to your Gemfile for Bundler:
26
18
  ```ruby
27
- require 'process_settings'
28
-
29
- ProcessSettings::Monitor.file_path = "/etc/process_settings/combined_process_settings.yml"
30
- ProcessSettings::Monitor.logger = logger
31
- ```
32
- ### Monitor Initialization
33
- The `ProcessSettings::Monitor` is a hybrid singleton. The class attribute `instance` returns
34
- the current instance. If not already set, this is lazy-created based on the above configuration.
35
-
36
- The monitor should be initialized with static (unchanging) context for your process:
37
- ```
38
- ProcessSettings::Monitor.static_context = {
39
- "service_name" => "frontend",
40
- "data_center" => "AWS-US-EAST-1"
41
- }
42
- ```
43
- The `static_context` is important because it is used to pre-filter settings for the process.
44
- For example, a setting that is targeted to `service_name: frontend` will match the above static context and
45
- be simplified to `true`. In other processes with a different `service_name`, such a targeted setting will be
46
- simplified to `false` and removed from memory.
47
-
48
- Note that the `static_context` as well as `dynamic_context` must use strings, not symbols, for both keys and values.
49
-
50
- ### Reading Settings
51
- For the following section, consider this `combined_process_settings.yml` file:
52
- ```
53
- ---
54
- - filename: frontend.yml
55
- settings:
56
- frontend:
57
- log_level: info
58
- - filename: frontend-microsite.yml
59
- target:
60
- domain: microsite.example.com
61
- settings:
62
- frontend:
63
- log_level: debug
64
- - meta:
65
- version: 27
66
- END: true
67
- ```
68
-
69
- To read a setting, application code should call the `[]` method on the `ProcessSettings` class. For example:
70
- ```
71
- log_level = ProcessSettings['frontend', 'log_level']
72
- => "info"
73
- ```
74
- #### ProcessSettings[] interface
75
- The `ProcessSettings[]` method delegates to `ProcessSettings::Monitor#[]` on the `instance`.
76
-
77
- `[]` interface:
78
-
79
- ```
80
- [](*path, dynamic_context: {}, required: true)
81
- ```
82
-
83
- |argument|description|
84
- |--------|-------------|
85
- |_path_ |A series of 1 or more comma-separated strings forming a path to navigate the `settings` hash, starting at the top.|
86
- |`dynamic_context:` |An optional hash of dynamic settings, used to target the settings. This will automatically be deep-merged with the static context. It may not contradict the static context. |
87
- |`required:` |A boolean indicating if the setting is required to be present. If a setting is missing, then if `required` is truthy, a `ProcesssSettings::SettingsPathNotFound` exception will be raised. Otherwise, `nil` will be returned. Default: `true`.
88
-
89
- Example with `dynamic_context`:
90
- ```
91
- log_level = ProcessSettings['frontend', 'log_level',
92
- dynamic_context: { "domain" => "microsite.example.com" }
93
- ]
94
- => "debug"
95
- ```
96
-
97
- Example with `required: true` (default) that was not found:
19
+ gem 'process_settings', git: 'git@github.com:Invoca/process_settings'
98
20
  ```
99
- http_version = ProcessSettings['frontend', 'http_version']
100
-
101
- exception raised!
102
21
 
103
- ProcessSettings::SettingsPathNotFound: No settings found for path ["frontend", "http_version"]
22
+ ## Usage
23
+ ### Initialization
24
+ To use the contextual logger, all you need to do is initailize the object with your existing logger
25
+ ```ruby
26
+ require 'process_settings'
104
27
  ```
105
28
 
106
- Here is the same example with `required: false`, applying a default value of `2`:
107
- ```
108
- http_version = ProcessSettings['frontend', 'http_version', required: false] || 2
109
- ```
29
+ TODO: Fill in here how to use the Monitor's instance method to get current settings, how to register for on_change callbacks, etc.
110
30
 
111
31
  ### Dynamic Settings
112
32
 
113
- The `ProcessSettings::Monitor` loads settings changes dynamically whenever the file changes,
114
- by using the [listen](https://github.com/guard/listen) gem which in turn uses the `INotify` module of the Linux kernel, or `FSEvents` on MacOS. There is no need to restart the process or send it a signal to tell it to reload changes.
115
-
116
- There are two ways to get access the latest settings from inside the process:
117
-
118
- #### Read Latest Setting Through `ProcessSettings[]`
119
-
120
- The simplest approach--as shown above--is to read the latest settings at any time through `ProcessSettings[]` (which delegates to `ProcessSettings::Monitor.instance`):
121
- ```
122
- http_version = ProcessSettings['frontend', 'http_version']
123
- ```
124
-
125
- #### Register an `on_change` Callback
126
- Alternatively, if you need to execute some code when there is a change, register a callback with `ProcessSettings::Monitor#on_change`:
127
- ```
128
- ProcessSettings::Monitor.instance.on_change do
129
- logger.level = ProcessSettings['frontend', 'log_level']
130
- end
131
- ```
132
- Note that all callbacks run sequentially on the shared change monitoring thread, so please be considerate!
133
-
134
- There is no provision for unregistering callbacks. Instead, replace the `instance` of the monitor with a new one.
135
-
136
- ## Targeting
137
- Each settings YAML file has an optional `target` key at the top level, next to `settings`.
138
-
139
- If there is no `target` key, the target defaults to `true`, meaning all processes are targeted for these settings. (However, the settings may be overridden by other YAML files. See "Precedence" below.)
140
-
141
- ### Hash Key-Values Are AND'd
142
- To `target` on context values, provide a hash of key-value pairs. All keys must match for the target to be met. For example, consider this target hash:
143
- ```
144
- target:
145
- service_name: frontend
146
- data_center: AWS-US-EAST-1
147
- ```
148
- This will be applied in any process that has `service_name == "frontend"` AND is running in `data_center == "AWS-US-EAST-1"`.
149
-
150
- ### Multiple Values Are OR'd
151
- Values may be set to an array, in which case the key matches if _any_ of the values matches. For example, consider this target hash:
152
- ```
153
- target:
154
- service_name: [frontend, auth]
155
- data_center: AWS-US-EAST-1
156
- ```
157
- This will be applied in any process that has (`service_name == "frontend"` OR `service_name == "auth"`) AND `data_center == "AWS-US-EAST-1"`.
33
+ In order to load changes dynamically, `ProcessSettings` relies on INotify module of the Linux kernel. On kernels that do not have this module (MacOS for example), you will see a warning on STDERR that changes will not be loaded while the process runs.
158
34
 
159
- ### Precedence
160
- The settings YAML files are always combined in alphabetical order by file path. Later settings take precedence over the earlier ones.
161
35
 
162
36
  ## Contributions
163
37
 
@@ -32,16 +32,15 @@ def parse_options(argv)
32
32
  options.verbose = false
33
33
  option_parser = OptionParser.new(argv) do |opt|
34
34
  opt.on('-v', '--verbose', 'Verbose mode.') { options.verbose = true }
35
- opt.on('-n', '--version=VERSION', 'Set version number.') { |value| options.version = value.to_i unless value.empty? }
36
35
  opt.on('-r', '--root_folder=ROOT') { |o| options.root_folder = o }
37
36
  opt.on('-o', '--output=FILENAME', 'Output file.') { |o| options.output_filename = o }
38
37
  opt.on('-i', '--initial=FILENAME', 'Initial settings file for version inference.') { |o| options.initial_filename = o }
39
38
  end
40
39
 
41
- if option_parser.parse! && options.root_folder && options.output_filename && (options.version || options.initial_filename)
40
+ if option_parser.parse! && options.root_folder && options.output_filename && (ENV['BUILD_NUMBER'] || options.initial_filename)
42
41
  options
43
42
  else
44
- warn "usage: #{PROGRAM_NAME} -r staging|production -o combined_process_settings.yml [--version=VERSION] [-i initial_combined_process_settings.yml] (-i required if --version= not set)"
43
+ warn "usage: #{PROGRAM_NAME} -r staging|production -o combined_process_settings.yml [-i initial_combined_process_settings.yml] (required if BUILD_NUMBER not set)"
45
44
  option_parser.summarize(STDERR)
46
45
  exit(1)
47
46
  end
@@ -83,7 +82,7 @@ options = parse_options(ARGV.dup)
83
82
 
84
83
  combined_settings = read_and_combine_settings(Pathname.new(options.root_folder) + SETTINGS_FOLDER)
85
84
 
86
- version_number = options.version || default_version_number(options.initial_filename)
85
+ version_number = ENV['BUILD_NUMBER']&.to_i || default_version_number(options.initial_filename)
87
86
  combined_settings << end_marker(version_number)
88
87
 
89
88
  yaml = combined_settings.to_yaml
@@ -34,6 +34,6 @@ system("rm -f tmp/combined_process_settings-A.yml tmp/combined_process_settings-
34
34
  system("sed '/^- meta:$/,$d' #{input_files[0]} > tmp/combined_process_settings-A.yml")
35
35
  system("sed '/^- meta:$/,$d' #{input_files[1]} > tmp/combined_process_settings-B.yml")
36
36
 
37
- system("diff -c tmp/combined_process_settings-A.yml tmp/combined_process_settings-B.yml | sed '1,3d'")
37
+ system("diff tmp/combined_process_settings-A.yml tmp/combined_process_settings-B.yml")
38
38
 
39
39
  system("rm -f tmp/combined_process_settings-A.yml tmp/combined_process_settings-B.yml")
@@ -107,7 +107,7 @@ if correlation_scorecard.any? { |_correllation_key, scorecard| scorecard[true].a
107
107
  system("rm -f tmp/old.yml tmp/new.yml")
108
108
  File.write("tmp/old.yml", ProcessSettings.plain_hash(best_correlation.last['__changes__']['old']).to_yaml)
109
109
  File.write("tmp/new.yml", ProcessSettings.plain_hash(best_correlation.last['__changes__']['new']).to_yaml)
110
- STDOUT << `diff -c tmp/old.yml tmp/new.yml | sed '1,3d'`
110
+ STDOUT << `diff tmp/old.yml tmp/new.yml`
111
111
  else
112
112
  puts "No best correlation found?"
113
113
  end
@@ -2,24 +2,13 @@
2
2
 
3
3
  module ProcessSettings
4
4
  # Module for mixing into `Hash` or other class with `[]` that you want to be able to index
5
- # with a hash path like: hash.mine('honeypot', 'answer_odds')
5
+ # with a hash path like: hash['app.service_name' => 'frontend']
6
6
  module HashPath
7
- # returns the value found at the given path
8
- # if the path is not found:
9
- # if a block is given, it is called and its value returned (may also raise an exception from the block)
10
- # else, the not_found_value: is returned
11
- def mine(*path_array, not_found_value: nil)
12
- path_array.is_a?(Enumerable) && path_array.size > 0 or raise ArgumentError, "path must be 1 or more keys; got #{path_array.inspect}"
13
- path_array.reduce(self) do |hash, key|
14
- if hash.has_key?(key)
15
- hash[key]
16
- else
17
- if block_given?
18
- break yield
19
- else
20
- break not_found_value
21
- end
22
- end
7
+ def [](key)
8
+ if key.is_a?(Hash)
9
+ HashPath.hash_at_path(self, key)
10
+ else
11
+ super
23
12
  end
24
13
  end
25
14
 
@@ -42,6 +31,24 @@ module ProcessSettings
42
31
  hash[path]
43
32
  end
44
33
  end
34
+
35
+ def set_hash_at_path(hash, path)
36
+ path.is_a?(Hash) or raise ArgumentError, "got unexpected non-hash value (#{hash[path]}"
37
+ case path.size
38
+ when 0
39
+ hash
40
+ when 1
41
+ path_key, path_value = path.first
42
+ if path_value.is_a?(Hash)
43
+ set_hash_at_path(remaining_hash, remaining_path)
44
+ else
45
+ hash[path_key] = path_value
46
+ end
47
+ else
48
+ raise ArgumentError, "path may have at most 1 key (got #{path.inspect})"
49
+ end
50
+ hash
51
+ end
45
52
  end
46
53
  end
47
54
  end
@@ -7,8 +7,6 @@ require 'listen'
7
7
  require 'active_support'
8
8
 
9
9
  module ProcessSettings
10
- class SettingsPathNotFound < StandardError; end
11
-
12
10
  class Monitor
13
11
  attr_reader :file_path, :min_polling_seconds, :logger
14
12
  attr_reader :static_context, :untargeted_settings, :statically_targeted_settings
@@ -28,8 +26,8 @@ module ProcessSettings
28
26
 
29
27
  path = File.dirname(@file_path)
30
28
 
31
- @listener = file_change_notifier.to(path) do |modified, added, _removed|
32
- if modified.include?(@file_path) || added.include?(@file_path)
29
+ @listener = file_change_notifier.to(path) do |modified, _added, _removed|
30
+ if modified.include?(@file_path)
33
31
  @logger.info("ProcessSettings::Monitor file #{@file_path} changed. Reloading.")
34
32
  load_untargeted_settings
35
33
 
@@ -56,10 +54,7 @@ module ProcessSettings
56
54
  end
57
55
 
58
56
  # Assigns a new static context. Recomputes statically_targeted_settings.
59
- # Keys must be strings or integers. No symbols.
60
57
  def static_context=(context)
61
- self.class.ensure_no_symbols(context)
62
-
63
58
  @static_context = context
64
59
 
65
60
  load_statically_targeted_settings(force_retarget: true)
@@ -67,32 +62,20 @@ module ProcessSettings
67
62
 
68
63
  # Returns the process settings value at the given `path` using the given `dynamic_context`.
69
64
  # (It is assumed that the static context was already set through static_context=.)
70
- # If nothing set at the given `path`:
71
- # if required, raises SettingsPathNotFound
72
- # else returns nil
73
- def targeted_value(*path, dynamic_context:, required: true)
65
+ # Returns `nil` if nothing set at the given `path`.
66
+ def targeted_value(path, dynamic_context)
74
67
  # Merging the static context in is necessary to make sure that the static context isn't shifting
75
68
  # this can be rather costly to do every time if the dynamic context is not changing
76
69
  # TODO: Warn in the case where dynamic context was attempting to change a static value
77
70
  # TODO: Cache the last used dynamic context as a potential optimization to avoid unnecessary deep merges
78
71
  full_context = dynamic_context.deep_merge(static_context)
79
- result = statically_targeted_settings.reduce(:not_found) do |latest_result, target_and_settings|
72
+ statically_targeted_settings.reduce(nil) do |result, target_and_settings|
80
73
  # find last value from matching targets
81
74
  if target_and_settings.target.target_key_matches?(full_context)
82
- if (value = target_and_settings.process_settings.json_doc.mine(*path, not_found_value: :not_found)) != :not_found
83
- latest_result = value
75
+ unless (value = HashPath.hash_at_path(target_and_settings.process_settings, path)).nil?
76
+ result = value
84
77
  end
85
78
  end
86
- latest_result
87
- end
88
-
89
- if result == :not_found
90
- if required
91
- raise SettingsPathNotFound, "no settings found for path #{path.inspect}"
92
- else
93
- nil
94
- end
95
- else
96
79
  result
97
80
  end
98
81
  end
@@ -141,20 +124,6 @@ module ProcessSettings
141
124
  @logger = new_logger
142
125
  Listen.logger = new_logger
143
126
  end
144
-
145
- def ensure_no_symbols(value)
146
- case value
147
- when Symbol
148
- raise ArgumentError, "symbol value #{value.inspect} found--should be String"
149
- when Hash
150
- value.each do |k, v|
151
- k.is_a?(Symbol) and raise ArgumentError, "symbol key #{k.inspect} found--should be String"
152
- ensure_no_symbols(v)
153
- end
154
- when Array
155
- value.each { |v| ensure_no_symbols(v) }
156
- end
157
- end
158
127
  end
159
128
 
160
129
  # Calls all registered on_change callbacks. Rescues any exceptions they may raise.
@@ -5,48 +5,31 @@ require_relative 'targeted_settings'
5
5
  module ProcessSettings
6
6
  # This class will override a file with a higher version file; it accounts for minor version number use
7
7
  module ReplaceVersionedFile
8
- class SourceVersionOlderError < StandardError; end
9
- class FileDoesNotExistError < StandardError; end
10
-
11
8
  class << self
12
- # Contracts
13
- # source_file_path must be present
14
- # destination_file_path must be present
15
- # source_file_path must exist on filesystem
16
- # source file version cannot be older destination version
17
9
  def replace_file_on_newer_file_version(source_file_path, destination_file_path)
18
10
  source_file_path.to_s == '' and raise ArgumentError, "source_file_path not present"
19
11
  destination_file_path.to_s == '' and raise ArgumentError, "destination_file_path not present"
20
- File.exist?(source_file_path) or raise FileDoesNotExistError, "source file '#{source_file_path}' does not exist"
21
- validate_source_version_is_not_older(source_file_path, destination_file_path)
22
12
 
23
13
  if source_version_is_newer?(source_file_path, destination_file_path)
24
- FileUtils.cp(source_file_path, destination_file_path)
14
+ FileUtils.mv(source_file_path, destination_file_path)
15
+ elsif source_file_path != destination_file_path # make sure we're not deleting destination file
16
+ if File.exist?(source_file_path)
17
+ FileUtils.remove_file(source_file_path) # clean up, remove left over file
18
+ end
25
19
  end
26
20
  end
27
21
 
28
22
  private
29
23
 
30
24
  def source_version_is_newer?(source_file_path, destination_file_path)
31
- if File.exist?(destination_file_path)
32
- source_version = ProcessSettings::TargetedSettings.from_file(source_file_path, only_meta: true).version
33
- destination_version = ProcessSettings::TargetedSettings.from_file(destination_file_path, only_meta: true).version
34
-
35
- source_version.to_f > destination_version.to_f
36
- else
37
- true
38
- end
39
- end
40
-
41
- def validate_source_version_is_not_older(source_file_path, destination_file_path)
42
- if File.exist?(destination_file_path)
43
- source_version = ProcessSettings::TargetedSettings.from_file(source_file_path, only_meta: true).version
44
- destination_version = ProcessSettings::TargetedSettings.from_file(destination_file_path, only_meta: true).version
45
-
46
- if source_version.to_f < destination_version.to_f
47
- raise SourceVersionOlderError,
48
- "source file '#{source_file_path}' is version #{source_version}"\
49
- " and destination file '#{destination_file_path}' is version #{destination_version}"
25
+ if File.exist?(source_file_path)
26
+ if File.exist?(destination_file_path)
27
+ source_version = ProcessSettings::TargetedSettings.from_file(source_file_path, only_meta: true).version
28
+ destination_version = ProcessSettings::TargetedSettings.from_file(destination_file_path, only_meta: true).version
29
+
30
+ Gem::Version.new(source_version) > Gem::Version.new(destination_version)
31
+ else
32
+ true
50
33
  end
51
34
  end
52
35
  end
@@ -21,5 +21,9 @@ module ProcessSettings
21
21
  def eql?(rhs)
22
22
  self == rhs
23
23
  end
24
+
25
+ def [](key)
26
+ @json_doc[key]
27
+ end
24
28
  end
25
29
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ProcessSettings
4
- VERSION = '0.4.0.pre.11'
4
+ VERSION = '0.4.0.1'
5
5
  end
@@ -7,8 +7,8 @@ require 'process_settings/monitor'
7
7
 
8
8
  module ProcessSettings
9
9
  class << self
10
- def [](*path, dynamic_context: {}, required: true)
11
- Monitor.instance.targeted_value(*path, dynamic_context: dynamic_context, required: required)
10
+ def [](value, dynamic_context = {})
11
+ Monitor.instance.targeted_value(value, dynamic_context)
12
12
  end
13
13
  end
14
14
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0.pre.11
4
+ version: 0.4.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Invoca
@@ -96,9 +96,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
96
  version: '0'
97
97
  required_rubygems_version: !ruby/object:Gem::Requirement
98
98
  requirements:
99
- - - ">"
99
+ - - ">="
100
100
  - !ruby/object:Gem::Version
101
- version: 1.3.1
101
+ version: '0'
102
102
  requirements: []
103
103
  rubyforge_project:
104
104
  rubygems_version: 2.6.13