prefab-cloud-ruby 1.6.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8cd1347e930a4820b28ae3943e100a09765efeb56ca423dc4e4f26b51e3543b
4
- data.tar.gz: 783119a33360850c31c425558735d356202e5365aa9f3ae3e88f776a22c7e231
3
+ metadata.gz: e0cc90e8b5acc15676a5101e6cbd902e37e38781ebb6d6fb33c892f938df3bbc
4
+ data.tar.gz: 68e1dbb144273aa9c54fb11efdf9333f2260a8805705d2aff47885719d995b7f
5
5
  SHA512:
6
- metadata.gz: 04b84fcdebab292f086d7909428ea8b77f0d7a53a2dfe5943716ba988ea778ddef0f3c0f09ed4ccddae048097e17f358699633213059ab3062ae04ae287acc99
7
- data.tar.gz: 2f65bdf10b36016e397413774bc676b20ed40d42f4ea4abcb7984bdd16afcbefd313e05a28505630cc9a818bcca950e60dcb41774d15e353d08f1e4d8d028966
6
+ metadata.gz: 9518b4ab1b19e6f7532fd29146b26bd0fd4e3527dbc4fde041b61b712a843db95f6f9355d8afb0b2496436aab711ec5f4a57075dbaf80c7afd4bb85c35d92b53
7
+ data.tar.gz: d2f4806025b4c965792653149ac9976b47ed4f687bbbb9b73cf58af091a55d4ad03affe49e6cd55aba132fa1a49f422f73900a2d295abb3afb2867cf204bb533
@@ -22,7 +22,7 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
  strategy:
24
24
  matrix:
25
- ruby-version: ['2.7', '3.0', '3.1']
25
+ ruby-version: ['2.7', '3.0', '3.3']
26
26
 
27
27
  steps:
28
28
  - uses: actions/checkout@v3
@@ -36,7 +36,9 @@ jobs:
36
36
  uses: ruby/setup-ruby@v1
37
37
  with:
38
38
  ruby-version: ${{ matrix.ruby-version }}
39
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
39
+ bundler-cache: false # runs 'bundle install' and caches installed gems automatically
40
+ - name: Install dependencies
41
+ run: bundle install --without development --jobs 4 --retry 3
40
42
  - name: Run tests
41
43
  run: bundle exec rake
42
44
  env:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.1 - 2024-03-28
4
+
5
+ - Performance optimizations (#178)
6
+ - Global context (#182)
7
+
3
8
  ## 1.6.0 - 2024-03-27
4
9
 
5
10
  - Use semantic_logger for internal logging (#173)
data/Gemfile CHANGED
@@ -7,8 +7,7 @@ gem 'google-protobuf', platforms: :ruby
7
7
  gem 'ld-eventsource'
8
8
  gem 'uuid'
9
9
 
10
- gem 'activesupport', '>= 4'
11
- gem 'actionpack', '>= 4'
10
+ gem 'activesupport', '>= 4'
12
11
 
13
12
  gem 'semantic_logger', require: "semantic_logger/sync"
14
13
 
data/Gemfile.lock CHANGED
@@ -1,23 +1,7 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
- actionpack (7.1.2)
5
- actionview (= 7.1.2)
6
- activesupport (= 7.1.2)
7
- nokogiri (>= 1.8.5)
8
- racc
9
- rack (>= 2.2.4)
10
- rack-session (>= 1.0.1)
11
- rack-test (>= 0.6.3)
12
- rails-dom-testing (~> 2.2)
13
- rails-html-sanitizer (~> 1.6)
14
- actionview (7.1.2)
15
- activesupport (= 7.1.2)
16
- builder (~> 3.1)
17
- erubi (~> 1.11)
18
- rails-dom-testing (~> 2.2)
19
- rails-html-sanitizer (~> 1.6)
20
- activesupport (7.1.2)
4
+ activesupport (7.1.3.2)
21
5
  base64
22
6
  bigdecimal
23
7
  concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -27,34 +11,48 @@ GEM
27
11
  minitest (>= 5.1)
28
12
  mutex_m
29
13
  tzinfo (~> 2.0)
30
- addressable (2.8.0)
31
- public_suffix (>= 2.0.2, < 5.0)
14
+ addressable (2.8.6)
15
+ public_suffix (>= 2.0.2, < 6.0)
32
16
  ansi (1.5.0)
33
17
  base64 (0.2.0)
34
- benchmark-ips (2.10.0)
35
- bigdecimal (3.1.4)
18
+ benchmark-ips (2.13.0)
19
+ bigdecimal (3.1.7)
36
20
  builder (3.2.4)
37
- concurrent-ruby (1.1.10)
21
+ concurrent-ruby (1.2.3)
38
22
  connection_pool (2.4.1)
39
- crass (1.0.6)
40
23
  descendants_tracker (0.0.4)
41
24
  thread_safe (~> 0.3, >= 0.3.1)
42
- docile (1.3.5)
43
- domain_name (0.5.20190701)
44
- unf (>= 0.0.5, < 1.0.0)
45
- drb (2.2.0)
46
- ruby2_keywords
47
- erubi (1.12.0)
48
- faraday (1.3.0)
25
+ docile (1.4.0)
26
+ domain_name (0.6.20240107)
27
+ drb (2.2.1)
28
+ faraday (1.10.3)
29
+ faraday-em_http (~> 1.0)
30
+ faraday-em_synchrony (~> 1.0)
31
+ faraday-excon (~> 1.1)
32
+ faraday-httpclient (~> 1.0)
33
+ faraday-multipart (~> 1.0)
49
34
  faraday-net_http (~> 1.0)
50
- multipart-post (>= 1.2, < 3)
51
- ruby2_keywords
35
+ faraday-net_http_persistent (~> 1.0)
36
+ faraday-patron (~> 1.0)
37
+ faraday-rack (~> 1.0)
38
+ faraday-retry (~> 1.0)
39
+ ruby2_keywords (>= 0.0.4)
40
+ faraday-em_http (1.0.0)
41
+ faraday-em_synchrony (1.0.0)
42
+ faraday-excon (1.1.0)
43
+ faraday-httpclient (1.0.1)
44
+ faraday-multipart (1.0.4)
45
+ multipart-post (~> 2)
52
46
  faraday-net_http (1.0.1)
53
- ffi (1.15.5)
54
- ffi-compiler (1.0.1)
55
- ffi (>= 1.0.0)
47
+ faraday-net_http_persistent (1.2.0)
48
+ faraday-patron (1.0.0)
49
+ faraday-rack (1.0.0)
50
+ faraday-retry (1.0.3)
51
+ ffi (1.16.3)
52
+ ffi-compiler (1.3.2)
53
+ ffi (>= 1.15.5)
56
54
  rake
57
- git (1.13.0)
55
+ git (1.19.1)
58
56
  addressable (~> 2.8)
59
57
  rchardet (~> 1.8)
60
58
  github_api (0.19.0)
@@ -63,20 +61,21 @@ GEM
63
61
  faraday (>= 0.8, < 2)
64
62
  hashie (~> 3.5, >= 3.5.2)
65
63
  oauth2 (~> 1.0)
66
- google-protobuf (3.22.2)
67
- googleapis-common-protos-types (1.5.0)
68
- google-protobuf (~> 3.14)
64
+ google-protobuf (3.25.3)
65
+ googleapis-common-protos-types (1.14.0)
66
+ google-protobuf (~> 3.18)
69
67
  hashie (3.6.0)
70
- highline (2.0.3)
71
- http (5.0.1)
72
- addressable (~> 2.3)
68
+ highline (3.0.1)
69
+ http (5.2.0)
70
+ addressable (~> 2.8)
71
+ base64 (~> 0.1)
73
72
  http-cookie (~> 1.0)
74
73
  http-form_data (~> 2.2)
75
- llhttp-ffi (~> 0.3.0)
76
- http-cookie (1.0.4)
74
+ llhttp-ffi (~> 0.5.0)
75
+ http-cookie (1.0.5)
77
76
  domain_name (~> 0.5)
78
77
  http-form_data (2.3.0)
79
- i18n (1.14.1)
78
+ i18n (1.14.4)
80
79
  concurrent-ruby (~> 1.0)
81
80
  juwelier (2.4.9)
82
81
  builder
@@ -90,34 +89,32 @@ GEM
90
89
  rake
91
90
  rdoc
92
91
  semver2
93
- jwt (2.2.2)
92
+ jwt (2.8.1)
93
+ base64
94
94
  kamelcase (0.0.2)
95
95
  semver2 (~> 3)
96
- ld-eventsource (2.2.0)
96
+ ld-eventsource (2.2.2)
97
97
  concurrent-ruby (~> 1.0)
98
98
  http (>= 4.4.1, < 6.0.0)
99
- llhttp-ffi (0.3.1)
99
+ llhttp-ffi (0.5.0)
100
100
  ffi-compiler (~> 1.0)
101
101
  rake (~> 13.0)
102
- loofah (2.21.4)
103
- crass (~> 1.0.2)
104
- nokogiri (>= 1.12.0)
105
102
  macaddr (1.7.2)
106
103
  systemu (~> 2.6.5)
107
- mini_portile2 (2.8.2)
108
- minitest (5.16.2)
109
- minitest-focus (1.3.1)
104
+ mini_portile2 (2.8.5)
105
+ minitest (5.22.3)
106
+ minitest-focus (1.4.0)
110
107
  minitest (>= 4, < 6)
111
- minitest-reporters (1.5.0)
108
+ minitest-reporters (1.6.1)
112
109
  ansi
113
110
  builder
114
111
  minitest (>= 5.0)
115
112
  ruby-progressbar
116
113
  multi_json (1.15.0)
117
114
  multi_xml (0.6.0)
118
- multipart-post (2.1.1)
115
+ multipart-post (2.4.0)
119
116
  mutex_m (0.2.0)
120
- nokogiri (1.15.2)
117
+ nokogiri (1.16.3)
121
118
  mini_portile2 (~> 2.8.2)
122
119
  racc (~> 1.4)
123
120
  oauth2 (1.4.11)
@@ -126,41 +123,32 @@ GEM
126
123
  multi_json (~> 1.3)
127
124
  multi_xml (~> 0.5)
128
125
  rack (>= 1.2, < 4)
129
- psych (3.3.1)
130
- public_suffix (4.0.6)
131
- racc (1.7.0)
132
- rack (3.0.6.1)
133
- rack-session (2.0.0)
134
- rack (>= 3.0.0)
135
- rack-test (2.1.0)
136
- rack (>= 1.3)
137
- rails-dom-testing (2.2.0)
138
- activesupport (>= 5.0.0)
139
- minitest
140
- nokogiri (>= 1.6)
141
- rails-html-sanitizer (1.6.0)
142
- loofah (~> 2.21)
143
- nokogiri (~> 1.14)
144
- rake (13.0.6)
126
+ psych (5.1.2)
127
+ stringio
128
+ public_suffix (5.0.4)
129
+ racc (1.7.3)
130
+ rack (3.0.10)
131
+ rake (13.1.0)
145
132
  rchardet (1.8.0)
146
- rdoc (6.3.3)
147
- ruby-progressbar (1.11.0)
148
- ruby2_keywords (0.0.4)
133
+ rdoc (6.6.3.1)
134
+ psych (>= 4.0.0)
135
+ ruby-progressbar (1.13.0)
136
+ ruby2_keywords (0.0.5)
149
137
  semantic_logger (4.15.0)
150
138
  concurrent-ruby (~> 1.0)
151
139
  semver2 (3.4.2)
152
- simplecov (0.18.5)
140
+ simplecov (0.22.0)
153
141
  docile (~> 1.1)
154
142
  simplecov-html (~> 0.11)
143
+ simplecov_json_formatter (~> 0.1)
155
144
  simplecov-html (0.12.3)
145
+ simplecov_json_formatter (0.1.4)
146
+ stringio (3.1.0)
156
147
  systemu (2.6.5)
157
148
  thread_safe (0.3.6)
158
- timecop (0.9.4)
149
+ timecop (0.9.8)
159
150
  tzinfo (2.0.6)
160
151
  concurrent-ruby (~> 1.0)
161
- unf (0.1.4)
162
- unf_ext
163
- unf_ext (0.0.8)
164
152
  uuid (2.3.9)
165
153
  macaddr (~> 1.0)
166
154
 
@@ -168,7 +156,6 @@ PLATFORMS
168
156
  ruby
169
157
 
170
158
  DEPENDENCIES
171
- actionpack (>= 4)
172
159
  activesupport (>= 4)
173
160
  benchmark-ips
174
161
  bundler
data/Rakefile CHANGED
@@ -9,42 +9,45 @@ rescue Bundler::BundlerError => e
9
9
  warn 'Run `bundle install` to install missing gems'
10
10
  exit e.status_code
11
11
  end
12
+
12
13
  require 'rake'
13
- require 'juwelier'
14
- Juwelier::Tasks.new do |gem|
15
- # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
16
- gem.name = 'prefab-cloud-ruby'
17
- gem.homepage = 'http://github.com/prefab-cloud/prefab-cloud-ruby'
18
- gem.license = 'MIT'
19
- gem.summary = %(Prefab Ruby Infrastructure)
20
- gem.description = %(Feature Flags, Live Config, and Dynamic Log Levels as a service)
21
- gem.email = 'jdwyer@prefab.cloud'
22
- gem.authors = ['Jeff Dwyer']
14
+ task default: :test
23
15
 
24
- # dependencies defined in Gemfile
25
- end
26
- Juwelier::RubygemsDotOrgTasks.new
27
- require 'rake/testtask'
28
- Rake::TestTask.new(:test) do |test|
29
- test.libs << 'lib' << 'test'
30
- test.pattern = 'test/**/test_*.rb'
31
- test.verbose = true
32
- end
16
+ unless ENV['CI']
17
+ require 'juwelier'
18
+ Juwelier::Tasks.new do |gem|
19
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
20
+ gem.name = 'prefab-cloud-ruby'
21
+ gem.homepage = 'http://github.com/prefab-cloud/prefab-cloud-ruby'
22
+ gem.license = 'MIT'
23
+ gem.summary = %(Prefab Ruby Infrastructure)
24
+ gem.description = %(Feature Flags, Live Config, and Dynamic Log Levels as a service)
25
+ gem.email = 'jdwyer@prefab.cloud'
26
+ gem.authors = ['Jeff Dwyer']
33
27
 
34
- desc 'Code coverage detail'
35
- task :simplecov do
36
- ENV['COVERAGE'] = 'true'
37
- Rake::Task['test'].execute
38
- end
28
+ # dependencies defined in Gemfile
29
+ end
30
+ Juwelier::RubygemsDotOrgTasks.new
31
+ require 'rake/testtask'
32
+ Rake::TestTask.new(:test) do |test|
33
+ test.libs << 'lib' << 'test'
34
+ test.pattern = 'test/**/test_*.rb'
35
+ test.verbose = true
36
+ end
39
37
 
40
- task default: :test
38
+ desc 'Code coverage detail'
39
+ task :simplecov do
40
+ ENV['COVERAGE'] = 'true'
41
+ Rake::Task['test'].execute
42
+ end
41
43
 
42
- require 'rdoc/task'
43
- Rake::RDocTask.new do |rdoc|
44
- version = File.exist?('VERSION') ? File.read('VERSION') : ''
44
+ require 'rdoc/task'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ''
45
47
 
46
- rdoc.rdoc_dir = 'rdoc'
47
- rdoc.title = "prefab-cloud-ruby #{version}"
48
- rdoc.rdoc_files.include('README*')
49
- rdoc.rdoc_files.include('lib/**/*.rb')
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "prefab-cloud-ruby #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
50
53
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.6.0
1
+ 1.6.1
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+
6
+ gemspec = Dir.glob(File.expand_path("../../*.gemspec", __FILE__)).first
7
+ spec = Gem::Specification.load(gemspec)
8
+
9
+ # Add the require paths to the $LOAD_PATH
10
+ spec.require_paths.each do |path|
11
+ full_path = File.expand_path("../" + path, __dir__)
12
+ $LOAD_PATH.unshift(full_path) unless $LOAD_PATH.include?(full_path)
13
+ end
14
+
15
+ spec.require_paths.each do |path|
16
+ require "./lib/prefab-cloud-ruby"
17
+ end
18
+
19
+ require 'prefab-cloud-ruby'
20
+
21
+ $prefab = Prefab::Client.new(collect_logger_counts: false, collect_evaluation_summaries: false,
22
+ context_upload_mode: :none)
23
+ $prefab.get('prefab.auth.allowed_origins')
24
+
25
+ puts '-' * 80
26
+
27
+ require 'allocation_stats'
28
+
29
+ $runs = 100
30
+
31
+ def measure(description)
32
+ puts "Measuring #{description}..."
33
+ stats = $prefab.with_context(user: { email_suffix: 'yahoo.com' }) do
34
+ AllocationStats.trace do
35
+ $runs.times do
36
+ yield
37
+ end
38
+ end
39
+ end
40
+
41
+ allocations = stats.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class)
42
+
43
+ if ENV['TOP']
44
+ puts allocations.sort_by_size.to_text.split("\n").first(20)
45
+ end
46
+
47
+ puts "Total allocations: #{allocations.all.values.map(&:size).sum}"
48
+ puts "Total memory: #{allocations.all.values.flatten.map(&:memsize).sum}"
49
+ puts stats.gc_profiler_report
50
+ end
51
+
52
+ measure "no-JIT context (#{$runs} runs)" do
53
+ $prefab.get('prefab.auth.allowed_origins')
54
+ end
55
+
56
+ puts "\n\n"
57
+
58
+ measure "with JIT context (#{$runs} runs)" do
59
+ $prefab.get('prefab.auth.allowed_origins', { a: { b: "c" } })
60
+ end
data/dev/benchmark ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+
6
+ gemspec = Dir.glob(File.expand_path("../../*.gemspec", __FILE__)).first
7
+ spec = Gem::Specification.load(gemspec)
8
+
9
+ # Add the require paths to the $LOAD_PATH
10
+ spec.require_paths.each do |path|
11
+ full_path = File.expand_path("../" + path, __dir__)
12
+ $LOAD_PATH.unshift(full_path) unless $LOAD_PATH.include?(full_path)
13
+ end
14
+
15
+ spec.require_paths.each do |path|
16
+ require "./lib/prefab-cloud-ruby"
17
+ end
18
+
19
+ require 'prefab-cloud-ruby'
20
+ require 'benchmark/ips'
21
+
22
+ prefab = Prefab::Client.new(collect_logger_counts: false, collect_evaluation_summaries: false,
23
+ context_upload_mode: :none)
24
+
25
+ prefab.get('prefab.auth.allowed_origins')
26
+
27
+ prefab.with_context(user: { email_suffix: 'yahoo.com' }) do
28
+ Benchmark.ips do |x|
29
+ x.report("noop") do
30
+ end
31
+
32
+ x.report('prefab.get') do
33
+ prefab.get('prefab.auth.allowed_origins')
34
+ end
35
+
36
+ x.report('prefab.get with jit context') do
37
+ prefab.get('prefab.auth.allowed_origins', { a: { b: "c" } })
38
+ end
39
+ end
40
+ end
data/lib/prefab/client.rb CHANGED
@@ -29,6 +29,9 @@ module Prefab
29
29
  end
30
30
 
31
31
  context.clear
32
+
33
+ Prefab::Context.global_context = @options.global_context
34
+
32
35
  # start config client
33
36
  config_client
34
37
  end
@@ -60,7 +60,7 @@ module Prefab
60
60
  @base_client.example_contexts_aggregator.record(context)
61
61
  end
62
62
 
63
- evaluation = _get(key, context)
63
+ evaluation = _get(key, properties)
64
64
 
65
65
  @base_client.context_shape_aggregator&.push(context)
66
66
 
@@ -168,7 +168,7 @@ module Prefab
168
168
  ]
169
169
  end.to_h
170
170
 
171
- @config_resolver.default_context = default_contexts || {}
171
+ Prefab::Context.default_context = default_contexts || {}
172
172
 
173
173
  configs.configs.each do |config|
174
174
  @config_loader.set(config, source)
@@ -5,8 +5,6 @@ module Prefab
5
5
  attr_accessor :project_env_id # this will be set by the config_client when it gets an API response
6
6
  attr_reader :local_store
7
7
 
8
- attr_accessor :default_context
9
-
10
8
  def initialize(base_client, config_loader)
11
9
  @lock = Concurrent::ReadWriteLock.new
12
10
  @local_store = {}
@@ -14,7 +12,6 @@ module Prefab
14
12
  @project_env_id = 0 # we don't know this yet, it is set from the API results
15
13
  @base_client = base_client
16
14
  @on_update = nil
17
- @default_context = {}
18
15
  make_local
19
16
  end
20
17
 
@@ -59,11 +56,13 @@ module Prefab
59
56
  end
60
57
 
61
58
  def make_context(properties)
62
- if properties == NO_DEFAULT_PROVIDED || properties.nil?
59
+ if properties.is_a?(Context)
60
+ properties
61
+ elsif properties == NO_DEFAULT_PROVIDED || properties.nil?
63
62
  Context.current
64
63
  else
65
- Context.merge_with_current(properties)
66
- end.merge_default(default_context || {})
64
+ Context.join(parent: Context.current, hash: properties, id: :jit)
65
+ end
67
66
  end
68
67
 
69
68
  private
@@ -8,18 +8,8 @@ module Prefab
8
8
  attr_reader :name
9
9
 
10
10
  def initialize(name, hash)
11
- @hash = {}
12
11
  @name = name.to_s
13
-
14
- merge!(hash)
15
- end
16
-
17
- def get(parts)
18
- @hash[parts]
19
- end
20
-
21
- def merge!(other)
22
- @hash = @hash.merge(other.transform_keys(&:to_s))
12
+ @hash = hash.transform_keys(&:to_s)
23
13
  end
24
14
 
25
15
  def to_h
@@ -27,13 +17,13 @@ module Prefab
27
17
  end
28
18
 
29
19
  def key
30
- "#{@name}:#{get('key')}"
20
+ "#{@name}:#{@hash['key']}"
31
21
  end
32
22
 
33
23
  def to_proto
34
24
  PrefabProto::Context.new(
35
25
  type: name,
36
- values: to_h.transform_values do |value|
26
+ values: @hash.transform_values do |value|
37
27
  ConfigValueWrapper.wrap(value)
38
28
  end
39
29
  )
@@ -44,17 +34,35 @@ module Prefab
44
34
  attr_reader :contexts, :seen_at
45
35
 
46
36
  class << self
37
+ def global_context=(context)
38
+ @global_context = join(hash: context, parent: nil, id: :global_context)
39
+ end
40
+
41
+ def global_context
42
+ @global_context ||= join(parent: nil, id: :global_context)
43
+ end
44
+
45
+ def default_context=(context)
46
+ @default_context = join(hash: context, parent: global_context, id: :default_context)
47
+
48
+ self.current.update_parent(@default_context)
49
+ end
50
+
51
+ def default_context
52
+ @default_context ||= join(parent: global_context, id: :default_context)
53
+ end
54
+
47
55
  def current=(context)
48
- Thread.current[THREAD_KEY] = context
56
+ Thread.current[THREAD_KEY] = join(hash: context || {}, parent: default_context, id: :block)
49
57
  end
50
58
 
51
59
  def current
52
- Thread.current[THREAD_KEY] ||= new
60
+ Thread.current[THREAD_KEY] ||= join(parent: default_context, id: :block)
53
61
  end
54
62
 
55
63
  def with_context(context)
56
64
  old_context = Thread.current[THREAD_KEY]
57
- Thread.current[THREAD_KEY] = new(context)
65
+ Thread.current[THREAD_KEY] = join(parent: default_context, hash: context, id: :block)
58
66
  yield
59
67
  ensure
60
68
  Thread.current[THREAD_KEY] = old_context
@@ -62,7 +70,7 @@ module Prefab
62
70
 
63
71
  def with_merged_context(context)
64
72
  old_context = Thread.current[THREAD_KEY]
65
- Thread.current[THREAD_KEY] = merge_with_current(context)
73
+ Thread.current[THREAD_KEY] = join(parent: current, hash: context, id: :merged)
66
74
  yield
67
75
  ensure
68
76
  Thread.current[THREAD_KEY] = old_context
@@ -77,51 +85,92 @@ module Prefab
77
85
  end
78
86
  end
79
87
 
80
- def initialize(context = {})
88
+ def self.join(hash: {}, parent: nil, id: :not_provided)
89
+ context = new(hash)
90
+ context.update_parent(parent)
91
+ context.instance_variable_set(:@id, id)
92
+ context
93
+ end
94
+
95
+ def initialize(hash = {})
81
96
  @contexts = {}
97
+ @flattened = {}
82
98
  @seen_at = Time.now.utc.to_i
99
+ warned = false
100
+
101
+ if hash.is_a?(Hash)
102
+ hash.map do |name, values|
103
+ unless values.is_a?(Hash)
104
+ warn "[DEPRECATION] Prefab contexts should be a hash with a key of the context name and a value of a hash."
105
+ values = { name => values }
106
+ name = BLANK_CONTEXT_NAME
107
+ end
83
108
 
84
- if context.is_a?(NamedContext)
85
- @contexts[context.name] = context
86
- elsif context.is_a?(Hash)
87
- context.map do |name, values|
88
- if values.is_a?(Hash)
89
- @contexts[name.to_s] = NamedContext.new(name, values)
90
- else
91
- warn '[DEPRECATION] Prefab contexts should be a hash with a key of the context name and a value of a hash.'
92
-
93
- @contexts[BLANK_CONTEXT_NAME] ||= NamedContext.new(BLANK_CONTEXT_NAME, {})
94
- @contexts[BLANK_CONTEXT_NAME].merge!({ name => values })
109
+ @contexts[name.to_s] = NamedContext.new(name, values)
110
+ values.each do |key, value|
111
+ @flattened[name.to_s + '.' + key.to_s] = value
95
112
  end
96
113
  end
97
114
  else
98
- raise ArgumentError, 'must be a Hash or a NamedContext'
115
+ raise ArgumentError, 'must be a Hash'
99
116
  end
100
117
  end
101
118
 
119
+ def update_parent(parent)
120
+ @parent = parent
121
+ end
122
+
102
123
  def blank?
103
124
  contexts.empty?
104
125
  end
105
126
 
106
127
  def set(name, hash)
107
128
  @contexts[name.to_s] = NamedContext.new(name, hash)
129
+ hash.each do |key, value|
130
+ @flattened[name.to_s + '.' + key.to_s] = value
131
+ end
108
132
  end
109
133
 
110
- def get(property_key)
111
- name, key = property_key.split('.', 2)
112
-
113
- if key.nil?
114
- name = BLANK_CONTEXT_NAME
115
- key = property_key
134
+ def get(property_key, scope: nil)
135
+ if !property_key.include?(".")
136
+ property_key = BLANK_CONTEXT_NAME + '.' + property_key
116
137
  end
117
138
 
118
- contexts[name]&.get(key)
139
+ if @flattened.key?(property_key)
140
+ @flattened[property_key]
141
+ else
142
+ scope ||= property_key.split('.').first
143
+
144
+ if @contexts[scope]
145
+ # If the key is in the present scope, parent values should not be used.
146
+ # We can consider the parent value clobbered by the present scope.
147
+ nil
148
+ else
149
+ @parent&.get(property_key, scope: scope)
150
+ end
151
+ end
119
152
  end
120
153
 
121
154
  def to_h
122
155
  contexts.transform_values(&:to_h)
123
156
  end
124
157
 
158
+ def to_s
159
+ "#<Prefab::Context:#{object_id} id=#{@id} #{to_h}>"
160
+ end
161
+
162
+ # Visualize a tree of the context up through its parents
163
+ #
164
+ # example:
165
+ #
166
+ # | jit: {"user"=>{"name"=>"Frank"}}
167
+ # |-- block: {"clock"=>{"timezone"=>"PST"}}
168
+ # |---- default_context: {"prefab-api-key"=>{"user-id"=>123}}
169
+ # |------ global_context: {"cpu"=>{"count"=>4, "speed"=>"2.4GHz"}, "clock"=>{"timezone"=>"UTC"}}
170
+ def tree(depth = 0)
171
+ "|" + ("-" * depth) + " #{id}: #{(" " * (30 - id.to_s.length - depth ))}#{to_h}\n" + (@parent&.tree(depth + 2) || '')
172
+ end
173
+
125
174
  def clear
126
175
  @contexts = {}
127
176
  end
@@ -175,5 +224,9 @@ module Prefab
175
224
  super
176
225
  end
177
226
  end
227
+
228
+ def id
229
+ @id
230
+ end
178
231
  end
179
232
  end
@@ -3,7 +3,7 @@
3
3
  module Prefab
4
4
  # Records the result of evaluating a config's criteria and forensics for reporting
5
5
  class Evaluation
6
- attr_reader :value
6
+ attr_reader :value, :context
7
7
 
8
8
  def initialize(config:, value:, value_index:, config_row_index:, context:, resolver:)
9
9
  @config = config
@@ -32,6 +32,7 @@ module Prefab
32
32
 
33
33
  def report(evaluation_summary_aggregator)
34
34
  return if @config.config_type == :LOG_LEVEL
35
+
35
36
  evaluation_summary_aggregator&.record(
36
37
  config_key: @config.key,
37
38
  config_type: @config.config_type,
@@ -15,7 +15,7 @@ module Prefab
15
15
  attr_reader :collect_sync_interval
16
16
  attr_reader :use_local_cache
17
17
  attr_reader :datafile
18
- attr_reader :disable_action_controller_logging
18
+ attr_reader :global_context
19
19
  attr_accessor :is_fork
20
20
 
21
21
  module ON_INITIALIZATION_FAILURE
@@ -59,7 +59,7 @@ module Prefab
59
59
  allow_telemetry_in_local_mode: false,
60
60
  x_datafile: ENV['PREFAB_DATAFILE'],
61
61
  x_use_local_cache: false,
62
- disable_action_controller_logging: false
62
+ global_context: {}
63
63
  )
64
64
  @api_key = api_key
65
65
  @namespace = namespace
@@ -79,8 +79,8 @@ module Prefab
79
79
  @collect_max_evaluation_summaries = collect_max_evaluation_summaries
80
80
  @allow_telemetry_in_local_mode = allow_telemetry_in_local_mode
81
81
  @use_local_cache = x_use_local_cache
82
- @disable_action_controller_logging = disable_action_controller_logging
83
82
  @is_fork = false
83
+ @global_context = global_context
84
84
 
85
85
  # defaults that may be overridden by context_upload_mode
86
86
  @collect_shapes = false
@@ -45,7 +45,6 @@ require 'prefab/context'
45
45
  require 'prefab/logger_client'
46
46
  require 'active_support/deprecation'
47
47
  require 'active_support'
48
- require 'action_controller/metal/strong_parameters'
49
48
  require 'prefab/client'
50
49
  require 'prefab/config_client_presenter'
51
50
  require 'prefab/config_client'
@@ -2,19 +2,18 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: prefab-cloud-ruby 1.6.0 ruby lib
5
+ # stub: prefab-cloud-ruby 1.6.1 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "prefab-cloud-ruby".freeze
9
- s.version = "1.6.0"
9
+ s.version = "1.6.1"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Jeff Dwyer".freeze]
14
- s.date = "2024-03-27"
14
+ s.date = "2024-03-28"
15
15
  s.description = "Feature Flags, Live Config, and Dynamic Log Levels as a service".freeze
16
16
  s.email = "jdwyer@prefab.cloud".freeze
17
- s.executables = ["console".freeze]
18
17
  s.extra_rdoc_files = [
19
18
  "CHANGELOG.md",
20
19
  "LICENSE.txt",
@@ -34,8 +33,10 @@ Gem::Specification.new do |s|
34
33
  "README.md",
35
34
  "Rakefile",
36
35
  "VERSION",
37
- "bin/console",
38
36
  "compile_protos.sh",
37
+ "dev/allocation_stats",
38
+ "dev/benchmark",
39
+ "dev/console",
39
40
  "lib/prefab-cloud-ruby.rb",
40
41
  "lib/prefab/client.rb",
41
42
  "lib/prefab/config_client.rb",
@@ -129,7 +130,6 @@ Gem::Specification.new do |s|
129
130
  s.add_runtime_dependency(%q<ld-eventsource>.freeze, [">= 0"])
130
131
  s.add_runtime_dependency(%q<uuid>.freeze, [">= 0"])
131
132
  s.add_runtime_dependency(%q<activesupport>.freeze, [">= 4"])
132
- s.add_runtime_dependency(%q<actionpack>.freeze, [">= 4"])
133
133
  s.add_runtime_dependency(%q<semantic_logger>.freeze, [">= 0"])
134
134
  s.add_development_dependency(%q<benchmark-ips>.freeze, [">= 0"])
135
135
  s.add_development_dependency(%q<bundler>.freeze, [">= 0"])
@@ -144,7 +144,6 @@ Gem::Specification.new do |s|
144
144
  s.add_dependency(%q<ld-eventsource>.freeze, [">= 0"])
145
145
  s.add_dependency(%q<uuid>.freeze, [">= 0"])
146
146
  s.add_dependency(%q<activesupport>.freeze, [">= 4"])
147
- s.add_dependency(%q<actionpack>.freeze, [">= 4"])
148
147
  s.add_dependency(%q<semantic_logger>.freeze, [">= 0"])
149
148
  s.add_dependency(%q<benchmark-ips>.freeze, [">= 0"])
150
149
  s.add_dependency(%q<bundler>.freeze, [">= 0"])
@@ -180,7 +180,7 @@ module CommonHelpers
180
180
  return
181
181
  end
182
182
 
183
- assert_equal expected, $stderr.string.split("\n")
183
+ assert_equal expected.uniq, $stderr.string.split("\n").uniq
184
184
 
185
185
  # restore since we've handled it
186
186
  $stderr = $oldstderr
@@ -408,6 +408,47 @@ class TestConfigResolver < Minitest::Test
408
408
  end
409
409
  end
410
410
 
411
+ def test_context_lookup
412
+ global_context = { cpu: { count: 4, speed: '2.4GHz' }, clock: { timezone: 'UTC' } }
413
+ default_context = { 'prefab-api-key' => { 'user-id' => 123 } }
414
+ local_context = { clock: { timezone: 'PST' }, user: { name: 'Ted', email: 'ted@example.com' } }
415
+ jit_context = { user: { name: 'Frank' } }
416
+
417
+ config = PrefabProto::Config.new( key: 'example', rows: [ PrefabProto::ConfigRow.new( values: [ PrefabProto::ConditionalValue.new( value: PrefabProto::ConfigValue.new(string: 'valueB2')) ]) ])
418
+
419
+ client = new_client(global_context: global_context, config: [config])
420
+
421
+ # we fake getting the default context from the API
422
+ Prefab::Context.default_context = default_context
423
+
424
+ resolver = client.resolver
425
+
426
+ client.with_context(local_context) do
427
+ context = resolver.get("example", jit_context).context
428
+
429
+ # This digs all the way to the global context
430
+ assert_equal 4, context.get('cpu.count')
431
+ assert_equal '2.4GHz', context.get('cpu.speed')
432
+
433
+ # This digs to the default context
434
+ assert_equal 123, context.get('prefab-api-key.user-id')
435
+
436
+ # This digs to the local context
437
+ assert_equal 'PST', context.get('clock.timezone')
438
+
439
+ # This uses the jit context
440
+ assert_equal 'Frank', context.get('user.name')
441
+
442
+ # This is nil in the jit context because `user` was clobbered
443
+ assert_nil context.get('user.email')
444
+
445
+ context = resolver.get("example").context
446
+
447
+ # But without the JIT clobbering, it is still set
448
+ assert_equal 'ted@example.com', context.get('user.email')
449
+ end
450
+ end
451
+
411
452
  private
412
453
 
413
454
  def resolver_for_namespace(namespace, loader, project_env_id: TEST_ENV_ID)
data/test/test_context.rb CHANGED
@@ -15,33 +15,17 @@ class TestContext < Minitest::Test
15
15
  assert_empty context.contexts
16
16
  end
17
17
 
18
- def test_initialize_with_named_context
19
- named_context = Prefab::Context::NamedContext.new('test', foo: 'bar')
20
- context = Prefab::Context.new(named_context)
21
- assert_equal 1, context.contexts.size
22
- assert_equal named_context, context.contexts['test']
23
- end
24
-
25
18
  def test_initialize_with_hash
26
19
  context = Prefab::Context.new(test: { foo: 'bar' })
27
20
  assert_equal 1, context.contexts.size
28
- assert_equal 'bar', context.contexts['test'].get('foo')
21
+ assert_equal 'bar', context.get("test.foo")
29
22
  end
30
23
 
31
24
  def test_initialize_with_multiple_hashes
32
25
  context = Prefab::Context.new(test: { foo: 'bar' }, other: { foo: 'baz' })
33
26
  assert_equal 2, context.contexts.size
34
- assert_equal 'bar', context.contexts['test'].get('foo')
35
- assert_equal 'baz', context.contexts['other'].get('foo')
36
- end
37
-
38
- def test_initialize_with_invalid_hash
39
- _, err = capture_io do
40
- Prefab::Context.new({ foo: 'bar', baz: 'qux' })
41
- end
42
-
43
- assert_match '[DEPRECATION] Prefab contexts should be a hash with a key of the context name and a value of a hash',
44
- err
27
+ assert_equal 'bar', context.get("test.foo")
28
+ assert_equal 'baz', context.get("other.foo")
45
29
  end
46
30
 
47
31
  def test_initialize_with_invalid_argument
@@ -56,33 +40,11 @@ class TestContext < Minitest::Test
56
40
 
57
41
  def test_current_set
58
42
  context = Prefab::Context.new(EXAMPLE_PROPERTIES)
59
- Prefab::Context.current = context
43
+ Prefab::Context.current = context.to_h
60
44
  assert_instance_of Prefab::Context, context
61
45
  assert_equal stringify(EXAMPLE_PROPERTIES), context.to_h
62
46
  end
63
47
 
64
- def test_merge_with_current
65
- context = Prefab::Context.new(EXAMPLE_PROPERTIES)
66
- Prefab::Context.current = context
67
- assert_equal stringify(EXAMPLE_PROPERTIES), context.to_h
68
-
69
- new_context = Prefab::Context.merge_with_current({ user: { key: 'brand-new', other: 'different' },
70
- address: { city: 'New York' } })
71
- assert_equal stringify({
72
- # Note that the user's `name` from the original
73
- # context is not included. This is because we don't _merge_ the new
74
- # properties if they collide with an existing context name. We _replace_
75
- # them.
76
- user: { key: 'brand-new', other: 'different' },
77
- team: EXAMPLE_PROPERTIES[:team],
78
- address: { city: 'New York' }
79
- }),
80
- new_context.to_h
81
-
82
- # the original/current context is unchanged
83
- assert_equal stringify(EXAMPLE_PROPERTIES), Prefab::Context.current.to_h
84
- end
85
-
86
48
  def test_with_context
87
49
  Prefab::Context.with_context(EXAMPLE_PROPERTIES) do
88
50
  context = Prefab::Context.current
@@ -105,9 +67,14 @@ class TestContext < Minitest::Test
105
67
 
106
68
  def test_with_context_merge_nesting
107
69
  Prefab::Context.with_context(EXAMPLE_PROPERTIES) do
108
- Prefab::Context.with_merged_context({ user: { key: 'abc', other: 'different' } }) do
70
+ Prefab::Context.with_merged_context({ user: { key: 'hij', other: 'different' } }) do
109
71
  context = Prefab::Context.current
110
- assert_equal({ 'user' => { 'key' => 'abc', 'other' => 'different' }, "team"=>{"key"=>"abc", "plan"=>"pro"} }, context.to_h)
72
+ assert_nil context.get('user.name')
73
+ assert_equal context.get('user.key'), 'hij'
74
+ assert_equal context.get('user.other'), 'different'
75
+
76
+ assert_equal context.get('team.key'), 'abc'
77
+ assert_equal context.get('team.plan'), 'pro'
111
78
  end
112
79
 
113
80
  context = Prefab::Context.current
@@ -187,6 +154,35 @@ class TestContext < Minitest::Test
187
154
  ), contexts.to_proto(namespace)
188
155
  end
189
156
 
157
+ def test_parent_lookup
158
+ global_context = { cpu: { count: 4, speed: '2.4GHz' }, clock: { timezone: 'UTC' } }
159
+ default_context = { 'prefab-api-key' => { 'user-id' => 123 } }
160
+ local_context = { clock: { timezone: 'PST' }, user: { name: 'Ted', email: 'ted@example.com' } }
161
+ jit_context = { user: { name: 'Frank' } }
162
+
163
+ Prefab::Context.global_context = global_context
164
+ Prefab::Context.default_context = default_context
165
+ Prefab::Context.current = local_context
166
+
167
+ context = Prefab::Context.join(parent: Prefab::Context.current, hash: jit_context, id: :jit)
168
+
169
+ # This digs all the way to the global context
170
+ assert_equal 4, context.get('cpu.count')
171
+ assert_equal '2.4GHz', context.get('cpu.speed')
172
+
173
+ # This digs to the default context
174
+ assert_equal 123, context.get('prefab-api-key.user-id')
175
+
176
+ # This digs to the local context
177
+ assert_equal 'PST', context.get('clock.timezone')
178
+
179
+ # This uses the jit context
180
+ assert_equal 'Frank', context.get('user.name')
181
+
182
+ # This is nil in the jit context because `user` was clobbered
183
+ assert_nil context.get('user.email')
184
+ end
185
+
190
186
  private
191
187
 
192
188
  def stringify(hash)
@@ -10,10 +10,7 @@ class TestExampleContextsAggregator < Minitest::Test
10
10
  aggregator = Prefab::ExampleContextsAggregator.new(client: MockBaseClient.new, max_contexts: 2,
11
11
  sync_interval: EFFECTIVELY_NEVER)
12
12
 
13
- context = Prefab::Context.new(
14
- user: { key: 'abc' },
15
- device: { key: 'def', mobile: true }
16
- )
13
+ context = Prefab::Context.new(user: { key: 'abc' }, device: { key: 'def', mobile: true })
17
14
 
18
15
  aggregator.record(context)
19
16
  assert_equal [context], aggregator.data
data/test/test_helper.rb CHANGED
@@ -11,7 +11,7 @@ Dir.glob(File.join(File.dirname(__FILE__), 'support', '**', '*.rb')).each do |fi
11
11
  require file
12
12
  end
13
13
 
14
- MiniTest::Test.class_eval do
14
+ Minitest::Test.class_eval do
15
15
  include CommonHelpers
16
16
  extend CommonHelpers
17
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prefab-cloud-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Dwyer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-27 00:00:00.000000000 Z
11
+ date: 2024-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -114,20 +114,6 @@ dependencies:
114
114
  - - ">="
115
115
  - !ruby/object:Gem::Version
116
116
  version: '4'
117
- - !ruby/object:Gem::Dependency
118
- name: actionpack
119
- requirement: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - ">="
122
- - !ruby/object:Gem::Version
123
- version: '4'
124
- type: :runtime
125
- prerelease: false
126
- version_requirements: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - ">="
129
- - !ruby/object:Gem::Version
130
- version: '4'
131
117
  - !ruby/object:Gem::Dependency
132
118
  name: semantic_logger
133
119
  requirement: !ruby/object:Gem::Requirement
@@ -214,8 +200,7 @@ dependencies:
214
200
  version: '0'
215
201
  description: Feature Flags, Live Config, and Dynamic Log Levels as a service
216
202
  email: jdwyer@prefab.cloud
217
- executables:
218
- - console
203
+ executables: []
219
204
  extensions: []
220
205
  extra_rdoc_files:
221
206
  - CHANGELOG.md
@@ -235,8 +220,10 @@ files:
235
220
  - README.md
236
221
  - Rakefile
237
222
  - VERSION
238
- - bin/console
239
223
  - compile_protos.sh
224
+ - dev/allocation_stats
225
+ - dev/benchmark
226
+ - dev/console
240
227
  - lib/prefab-cloud-ruby.rb
241
228
  - lib/prefab/client.rb
242
229
  - lib/prefab/config_client.rb
File without changes