fino-redis 1.3.0 → 1.4.0

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: 9974c2bc5e434114c5184d48f9c7d2654437c669e27b016fedd4694fb9404201
4
- data.tar.gz: 1f74ae02ccc9e4ff3c23a25a23f37f34b2a4f3eec45bd7b3f12bcaefe6e9121f
3
+ metadata.gz: bf8532d7c9300576fc53693bc44a1ae65cecc2177c7cffe1ced5e233c915272a
4
+ data.tar.gz: 0a8d663f550775de858394e53c6ee522e07d0a90fe257fd1c6d5a16773a743cc
5
5
  SHA512:
6
- metadata.gz: 1892e8b1672e070d5c608a21f2350f510fd5555434603c82296863bd8344eee9793da4102d017050246e131575dffdc83cb146386f139489b5b4dc753f8e9e44
7
- data.tar.gz: 281ed65e05cf646c8672ccd85be6f85ee53ee1a3d7b74b7d78ea0ff86a4d1ce7151f014b127d1d49e4817e351cbcb456f9945c2b5f9511e30f1bdce245fed7b8
6
+ metadata.gz: c7257b235d5755fb4474fb5e3a478b8e97b0a80a78b54b11967faf871bf70f3e34e3f7a4f49da1cf24729df12052617c6ec1b9fd1ebff255f5cccd40daf8937c
7
+ data.tar.gz: 5c44aa7255f8e5bb2ea16a8167acd64385e031a22635ebd583ebeac17aead3dd5c8acf504bd2134c4f550c5f3dc329d88b3593df9bf4e2676ed94037cffbb073
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Fino
2
2
 
3
- ⚠️ Fino is under active development. API changes are possible ⚠️
3
+ ⚠️ Fino in active development phase at wasn't properly battle tested in production just yet. Give us a star and stay tuned for Production test results and new features
4
4
 
5
5
  Fino is a dynamic settings engine for Ruby and Rails
6
6
 
@@ -23,10 +23,15 @@ Fino.configure do
23
23
  settings do
24
24
  setting :maintenance_mode, :boolean, default: false
25
25
 
26
+ setting :api_rate_limit,
27
+ :integer,
28
+ default: 1000,
29
+ description: "Maximum API requests per minute per user to prevent abuse"
30
+
26
31
  section :openai, label: "OpenAI" do
27
32
  setting :model,
28
33
  :string,
29
- default: "gpt-4o",
34
+ default: "gpt-5",
30
35
  description: "OpenAI model"
31
36
 
32
37
  setting :temperature,
@@ -51,7 +56,7 @@ end
51
56
  ### Work with settings
52
57
 
53
58
  ```ruby
54
- Fino.value(:model, at: :openai) #=> "gpt-4o"
59
+ Fino.value(:model, at: :openai) #=> "gpt-5"
55
60
  Fino.value(:temperature, at: :openai) #=> 0.7
56
61
 
57
62
  Fino.values(:model, :temperature, at: :openai) #=> ["gpt-4", 0.7]
@@ -60,12 +65,46 @@ Fino.set(model: "gpt-5", at: :openai)
60
65
  Fino.value(:model, at: :openai) #=> "gpt-5"
61
66
  ```
62
67
 
63
- ### Manage settings via UI
68
+ ### Overrides
64
69
 
65
70
  ```ruby
66
- gem "fino-rails"
71
+ Fino.value(:model, at: :openai) #=> "gpt-5"
72
+
73
+ Fino.set(model: "gpt-5", at: :openai, overrides: { "qa" => "our_local_model_not_to_pay_to_sam_altman" })
74
+
75
+ Fino.value(:model, at: :openai) #=> "gpt-5"
76
+ Fino.value(:model, at: :openai, for: "qa") #=> "our_local_model_not_to_pay_to_sam_altman"
77
+ ```
78
+
79
+ ### A/B testing
80
+
81
+ ```ruby
82
+ Fino.value(:model, at: :openai) #=> "gpt-5"
83
+
84
+ # "gpt-5" becomes the control variant value and a 20.0% variant is created with value "gpt-6"
85
+ Fino.set(model: "gpt-5", at: :openai, variants: { 20.0 => "gpt-6" })
86
+
87
+ Fino.setting(:model, at: :openai).experiment.variant(for: "user_1") #=> #<Fino::AbTesting::Variant percentage: 20.0, value: "gpt-6">
88
+
89
+ # Picked variant is sticked to the user
90
+ Fino.value(:model, at: :openai, for: "user_1") #=> "gpt-6"
91
+ Fino.value(:model, at: :openai, for: "user_1") #=> "gpt-6"
92
+
93
+ Fino.value(:model, at: :openai, for: "user_2") #=> "gpt-5"
67
94
  ```
68
95
 
96
+ ## Rails integration
97
+
98
+ Fino easily integrates with Rails. Just add the gem to your Gemfile:
99
+
100
+ ```
101
+ gem "fino-rails", require: false
102
+ ```
103
+
104
+ to get built-in UI engine for your settings!
105
+
106
+ ### UI engine
107
+
69
108
  Mount Fino Rails engine in your `config/routes.rb`:
70
109
 
71
110
  ```ruby
@@ -74,4 +113,84 @@ Rails.application.routes.draw do
74
113
  end
75
114
  ```
76
115
 
77
- <img width="1229" height="641" alt="Screenshot 2025-09-04 at 16 01 51" src="https://github.com/user-attachments/assets/646df84c-c25b-4890-9637-c481e18c9bd4" />
116
+ ### Configuration
117
+
118
+ ```ruby
119
+ Rails.application.configure do
120
+ config.fino.instrument = true
121
+ config.fino.log = true
122
+ config.fino.cache_within_request = false
123
+ config.fino.preload_before_request = true
124
+ end
125
+ ```
126
+
127
+ <img width="1493" height="676" alt="Screenshot 2025-09-19 at 13 09 06" src="https://github.com/user-attachments/assets/19b6147a-e18c-41cf-aac7-99111efcc9d5" />
128
+
129
+ <img width="1775" height="845" alt="Screenshot 2025-09-19 at 13 09 33" src="https://github.com/user-attachments/assets/c0010abd-285d-43d0-ae5d-ce0edb781309" />
130
+
131
+ ## Performance tweaks
132
+
133
+ 1. In Memory cache
134
+
135
+ Fino provides in-memory settings caching functionality which will store settings received from adaper in memory for
136
+ a very quick access. As this kind of cache is not distributed between machines, but belongs to each process
137
+ separately, it's impossible to invalidate all at once, so be aware that setting update application time will depend
138
+ on cache TTL you configure
139
+
140
+ ```ruby
141
+ Fino.configure do
142
+ # ...
143
+ cache { Fino::Cache::Memory.new(expires_in: 3.seconds) }
144
+ # ...
145
+ end
146
+ ```
147
+
148
+ 2. Request scoped cache
149
+
150
+ When using Fino in Rails context it's possible to cache settings within request, in current thread storage. This is
151
+ safe way to cache settings as it's lifetime is limited, thus it is enabled by default
152
+
153
+ ```ruby
154
+ Rails.application.configure do
155
+ config.fino.cache_within_request = true
156
+ end
157
+ ```
158
+
159
+ 3. Preloading
160
+
161
+ In Rails context it is possible to tell Fino to preload multiple settings before processing request in a single
162
+ adapter call. Preloading is recommended for requests that use multiple different settings in their logic
163
+
164
+ ```ruby
165
+ # Preload all settings
166
+ Rails.application.configure do
167
+ config.fino.preload_before_request = true
168
+ end
169
+
170
+ # Preload specific subset of settings depending on request
171
+ Rails.application.configure do
172
+ config.fino.preload_before_request = ->(request) {
173
+ case request.path
174
+ when "request/using/all/settings"
175
+ true
176
+ when "request/not/using/settings"
177
+ false
178
+ when "request/using/specific/settings"
179
+ [
180
+ :api_rate_limit,
181
+ openai: [:model, :temperature]
182
+ ]
183
+ end
184
+ }
185
+ end
186
+ ```
187
+
188
+ ## Releasing
189
+
190
+ `rake release`
191
+
192
+ ## Contributing
193
+
194
+ 1. Fork it
195
+ 2. Do contribution
196
+ 6. Create Pull Request into this repo
@@ -6,6 +6,8 @@ class Fino::Redis::Adapter
6
6
  using Fino::CustomRedisScripts
7
7
 
8
8
  DEFAULT_REDIS_NAMESPACE = "fino"
9
+ SETTINGS_NAMESPACE = "s"
10
+ PERSISTED_SETTINGS_KEYS_REDIS_KEY = "psl"
9
11
  SCOPE_PREFIX = "s"
10
12
  VARIANT_PREFIX = "v"
11
13
  VALUE_KEY = "v"
@@ -15,8 +17,16 @@ class Fino::Redis::Adapter
15
17
  @redis_namespace = namespace
16
18
  end
17
19
 
18
- def read(setting_definition)
19
- redis.hgetall(redis_key_for(setting_definition))
20
+ def read(setting_key)
21
+ redis.hgetall(redis_key_for(setting_key))
22
+ end
23
+
24
+ def read_multi(setting_keys)
25
+ keys = setting_keys.map { |setting_key| redis_key_for(setting_key) }
26
+
27
+ redis.pipelined do |pipeline|
28
+ keys.each { |key| pipeline.hgetall(key) }
29
+ end
20
30
  end
21
31
 
22
32
  def write(setting_definition, value, overrides, variants)
@@ -29,27 +39,35 @@ class Fino::Redis::Adapter
29
39
  end
30
40
 
31
41
  variants.each do |variant|
32
- next if variant.value == Fino::Variant::CONTROL
42
+ next if variant.value == Fino::AbTesting::Variant::CONTROL_VALUE
33
43
 
34
44
  hash["#{VARIANT_PREFIX}/#{variant.percentage}/#{VALUE_KEY}"] = serialize_value.call(variant.value)
35
45
  end
36
46
 
37
- redis.mapped_hreplace(redis_key_for(setting_definition), hash)
47
+ redis.multi do |r|
48
+ r.mapped_hreplace(redis_key_for(setting_definition.key), hash)
49
+ r.sadd(build_redis_key(PERSISTED_SETTINGS_KEYS_REDIS_KEY), setting_definition.key)
50
+ end
38
51
  end
39
52
 
40
- def read_multi(setting_definitions)
41
- keys = setting_definitions.map { |definition| redis_key_for(definition) }
53
+ def read_persisted_setting_keys
54
+ redis.smembers(build_redis_key(PERSISTED_SETTINGS_KEYS_REDIS_KEY))
55
+ end
42
56
 
43
- redis.pipelined do |pipeline|
44
- keys.each { |key| pipeline.hgetall(key) }
57
+ def clear(setting_key)
58
+ _, cleared = redis.multi do |r|
59
+ r.del(redis_key_for(setting_key))
60
+ r.srem(build_redis_key(PERSISTED_SETTINGS_KEYS_REDIS_KEY), setting_key)
45
61
  end
62
+
63
+ cleared == 1
46
64
  end
47
65
 
48
66
  def fetch_value_from(raw_adapter_data)
49
67
  raw_adapter_data.key?(VALUE_KEY) ? raw_adapter_data.delete(VALUE_KEY) : Fino::EMPTINESS
50
68
  end
51
69
 
52
- def fetch_scoped_values_from(raw_adapter_data)
70
+ def fetch_raw_overrides_from(raw_adapter_data)
53
71
  raw_adapter_data.each_with_object({}) do |(key, value), memo|
54
72
  next unless key.start_with?("#{SCOPE_PREFIX}/")
55
73
 
@@ -72,7 +90,11 @@ class Fino::Redis::Adapter
72
90
 
73
91
  attr_reader :redis, :redis_namespace
74
92
 
75
- def redis_key_for(setting_definition)
76
- "#{redis_namespace}:#{setting_definition.path.join(':')}"
93
+ def redis_key_for(setting_key)
94
+ build_redis_key(SETTINGS_NAMESPACE, setting_key)
95
+ end
96
+
97
+ def build_redis_key(*parts)
98
+ [redis_namespace, *parts].join(":")
77
99
  end
78
100
  end
@@ -1,12 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fino::CustomRedisScripts
4
+ module_function
5
+
6
+ def mapped_hreplace(connection, key, values_mapping)
7
+ connection.del(key)
8
+ connection.mapped_hmset(key, values_mapping)
9
+ end
10
+
4
11
  refine Redis do
5
12
  def mapped_hreplace(key, values_mapping)
6
- multi do |r|
7
- r.del(key)
8
- r.mapped_hmset(key, values_mapping)
13
+ multi do |multi|
14
+ Fino::CustomRedisScripts.mapped_hreplace(multi, key, values_mapping)
9
15
  end
10
16
  end
11
17
  end
18
+
19
+ refine Redis::MultiConnection do
20
+ def mapped_hreplace(key, values_mapping)
21
+ Fino::CustomRedisScripts.mapped_hreplace(self, key, values_mapping)
22
+ end
23
+ end
12
24
  end
data/lib/fino/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fino
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  REQUIRED_RUBY_VERSION = ">= 3.0.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fino-redis
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Egor Iskrenkov
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 1.3.0
18
+ version: 1.4.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 1.3.0
25
+ version: 1.4.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: redis
28
28
  requirement: !ruby/object:Gem::Requirement