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 +4 -4
- data/README.md +125 -6
- data/lib/fino/redis/adapter.rb +33 -11
- data/lib/fino/redis/custom_redis_scripts.rb +15 -3
- data/lib/fino/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf8532d7c9300576fc53693bc44a1ae65cecc2177c7cffe1ced5e233c915272a
|
|
4
|
+
data.tar.gz: 0a8d663f550775de858394e53c6ee522e07d0a90fe257fd1c6d5a16773a743cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c7257b235d5755fb4474fb5e3a478b8e97b0a80a78b54b11967faf871bf70f3e34e3f7a4f49da1cf24729df12052617c6ec1b9fd1ebff255f5cccd40daf8937c
|
|
7
|
+
data.tar.gz: 5c44aa7255f8e5bb2ea16a8167acd64385e031a22635ebd583ebeac17aead3dd5c8acf504bd2134c4f550c5f3dc329d88b3593df9bf4e2676ed94037cffbb073
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Fino
|
|
2
2
|
|
|
3
|
-
⚠️ Fino
|
|
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-
|
|
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-
|
|
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
|
-
###
|
|
68
|
+
### Overrides
|
|
64
69
|
|
|
65
70
|
```ruby
|
|
66
|
-
|
|
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
|
-
|
|
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
|
data/lib/fino/redis/adapter.rb
CHANGED
|
@@ -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(
|
|
19
|
-
redis.hgetall(redis_key_for(
|
|
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::
|
|
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.
|
|
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
|
|
41
|
-
|
|
53
|
+
def read_persisted_setting_keys
|
|
54
|
+
redis.smembers(build_redis_key(PERSISTED_SETTINGS_KEYS_REDIS_KEY))
|
|
55
|
+
end
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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(
|
|
76
|
-
|
|
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 |
|
|
7
|
-
|
|
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
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.
|
|
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.
|
|
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.
|
|
25
|
+
version: 1.4.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: redis
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|