prefab-cloud-ruby 1.6.0 → 1.6.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
  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