process_settings 0.4.0.pre.11 → 0.4.0.1

Sign up to get free protection for your applications and to get access to all the features.
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