rollout 2.6.2 → 3.2.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 +5 -5
- data/.github/workflows/release.yml +54 -28
- data/.github/workflows/test.yml +148 -6
- data/.gitignore +2 -0
- data/README.md +105 -25
- data/Rakefile +25 -2
- data/docs/upgrading-to-v3.md +110 -0
- data/lib/rollout/feature.rb +28 -29
- data/lib/rollout/feature_state.rb +44 -0
- data/lib/rollout/logging.rb +32 -76
- data/lib/rollout/version.rb +1 -1
- data/lib/rollout.rb +65 -52
- data/rollout.gemspec +6 -5
- data/spec/rollout/feature_spec.rb +70 -39
- data/spec/rollout/feature_state_spec.rb +219 -0
- data/spec/rollout/logging_spec.rb +81 -110
- data/spec/rollout/memory_backend_contract_spec.rb +8 -0
- data/spec/rollout_spec.rb +155 -586
- data/spec/spec_helper.rb +83 -10
- data/spec/support/backend_contract.rb +95 -0
- data/spec/support/history_contract.rb +174 -0
- data/spec/support/rollout_backend_integration.rb +77 -0
- metadata +14 -29
- data/.circleci/config.yml +0 -119
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cadea20a7d731c5538e5d6b6b19b34075843230fd5c6d75ffc4c269ce667a63f
|
|
4
|
+
data.tar.gz: '06483d373ff87c4e0537ab73d57be5bba089d43a4abacd87168bdf338cb4ca5e'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 515d4d646e3c4c2c2fee20d77088a9dd22fe5730dd605764abfbc0e189591f7eaaf076830940358500e05eed30f456b9389f8843bad1df283d01b795eae6e935
|
|
7
|
+
data.tar.gz: c41503b0e1acff3c3c99de9b08d7c06167f2d4a2f7c7fc493230c11c70f633dcae0bc4c5a7028d04e11b01dfd7edfb6eaaf6d33255c559cd4b93fda206e10427
|
|
@@ -3,44 +3,70 @@ name: Release
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
tags:
|
|
6
|
-
- 'v*'
|
|
6
|
+
- 'rollout/v*'
|
|
7
|
+
- 'rollout-redis-adapter/v*'
|
|
8
|
+
- 'rollout-active_record-adapter/v*'
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
id-token: write
|
|
7
13
|
|
|
8
14
|
jobs:
|
|
9
|
-
|
|
15
|
+
publish:
|
|
10
16
|
runs-on: ubuntu-latest
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- 6379:6379
|
|
16
|
-
options: >-
|
|
17
|
-
--health-cmd "redis-cli ping"
|
|
18
|
-
--health-interval 10s
|
|
19
|
-
--health-timeout 5s
|
|
20
|
-
--health-retries 5
|
|
17
|
+
outputs:
|
|
18
|
+
name: ${{ steps.package.outputs.name }}
|
|
19
|
+
version: ${{ steps.package.outputs.version }}
|
|
20
|
+
previous_tag: ${{ steps.package.outputs.previous_tag }}
|
|
21
21
|
steps:
|
|
22
22
|
- name: Checkout code
|
|
23
|
-
uses: actions/checkout@
|
|
23
|
+
uses: actions/checkout@v5
|
|
24
|
+
with:
|
|
25
|
+
fetch-depth: 0
|
|
24
26
|
- name: Setup Ruby
|
|
25
27
|
uses: ruby/setup-ruby@v1
|
|
26
28
|
with:
|
|
27
|
-
|
|
28
|
-
- name:
|
|
29
|
-
|
|
29
|
+
ruby-version: '3.3'
|
|
30
|
+
- name: Select package
|
|
31
|
+
id: package
|
|
32
|
+
run: |
|
|
33
|
+
case "$GITHUB_REF_NAME" in
|
|
34
|
+
rollout/v*) name=rollout; directory=. ;;
|
|
35
|
+
rollout-redis-adapter/v*) name=rollout-redis-adapter; directory=rollout-redis-adapter ;;
|
|
36
|
+
rollout-active_record-adapter/v*) name=rollout-active_record-adapter; directory=rollout-active_record-adapter ;;
|
|
37
|
+
*) echo "Unexpected tag $GITHUB_REF_NAME" >&2; exit 1 ;;
|
|
38
|
+
esac
|
|
39
|
+
version="${GITHUB_REF_NAME##*/v}"
|
|
40
|
+
spec_version=$(ruby -e 'puts Gem::Specification.load(ARGV[0]).version' "$directory/$name.gemspec")
|
|
41
|
+
if [ "$spec_version" != "$version" ]; then
|
|
42
|
+
echo "Tag version $version does not match gemspec $spec_version" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
previous_tag=$(git tag --list "$name/v*" --sort=-version:refname | awk -v current="$GITHUB_REF_NAME" '$0 != current { print; exit }')
|
|
46
|
+
previous_tag=${previous_tag:-v2.6.2}
|
|
47
|
+
echo "name=$name" >> "$GITHUB_OUTPUT"
|
|
48
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
49
|
+
echo "directory=$directory" >> "$GITHUB_OUTPUT"
|
|
50
|
+
echo "previous_tag=$previous_tag" >> "$GITHUB_OUTPUT"
|
|
51
|
+
- name: Build selected gem
|
|
52
|
+
working-directory: ${{ steps.package.outputs.directory }}
|
|
53
|
+
run: gem build "${{ steps.package.outputs.name }}.gemspec"
|
|
54
|
+
- name: Configure RubyGems credentials
|
|
55
|
+
uses: rubygems/configure-rubygems-credentials@v2.1.0
|
|
56
|
+
- name: Publish selected gem
|
|
57
|
+
working-directory: ${{ steps.package.outputs.directory }}
|
|
58
|
+
run: gem push "${{ steps.package.outputs.name }}-${{ steps.package.outputs.version }}.gem"
|
|
59
|
+
|
|
60
|
+
github-release:
|
|
61
|
+
needs: publish
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
30
64
|
- name: Create GitHub Release
|
|
31
|
-
uses: softprops/action-gh-release@
|
|
65
|
+
uses: softprops/action-gh-release@v3
|
|
32
66
|
with:
|
|
33
67
|
tag_name: ${{ github.ref }}
|
|
34
|
-
name: ${{
|
|
68
|
+
name: ${{ needs.publish.outputs.name }} ${{ needs.publish.outputs.version }}
|
|
35
69
|
generate_release_notes: true
|
|
70
|
+
previous_tag: ${{ needs.publish.outputs.previous_tag }}
|
|
36
71
|
draft: false
|
|
37
|
-
prerelease: false
|
|
38
|
-
- name: Set up RubyGems credentials
|
|
39
|
-
env:
|
|
40
|
-
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
|
41
|
-
run: |
|
|
42
|
-
mkdir -p ~/.gem
|
|
43
|
-
echo ":rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials
|
|
44
|
-
chmod 0600 ~/.gem/credentials
|
|
45
|
-
- name: Release to RubyGems
|
|
46
|
-
run: bundle exec rake release
|
|
72
|
+
prerelease: false
|
data/.github/workflows/test.yml
CHANGED
|
@@ -4,13 +4,115 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
branches:
|
|
6
6
|
- master
|
|
7
|
+
- v3
|
|
7
8
|
pull_request:
|
|
8
|
-
|
|
9
|
-
- master
|
|
9
|
+
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
12
|
-
|
|
12
|
+
core:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
ruby-version: ['3.3', '3.2', '3.1', '3.0', '2.7', '2.6', '2.5', '2.4']
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v5
|
|
20
|
+
- name: Setup Ruby
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
- name: Run core tests
|
|
26
|
+
run: |
|
|
27
|
+
mkdir -p test_results
|
|
28
|
+
bundle exec rspec spec --format progress --format RspecJunitFormatter --out test_results/rspec.xml
|
|
29
|
+
- name: Upload test results
|
|
30
|
+
if: always()
|
|
31
|
+
uses: actions/upload-artifact@v6
|
|
32
|
+
with:
|
|
33
|
+
name: core-test-results-${{ matrix.ruby-version }}
|
|
34
|
+
path: test_results/rspec.xml
|
|
35
|
+
retention-days: 30
|
|
36
|
+
|
|
37
|
+
redis:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
strategy:
|
|
40
|
+
matrix:
|
|
41
|
+
ruby-version: ['3.3', '3.2', '3.1', '3.0', '2.7', '2.6', '2.5', '2.4']
|
|
42
|
+
services:
|
|
43
|
+
redis:
|
|
44
|
+
image: redis:7-alpine
|
|
45
|
+
ports:
|
|
46
|
+
- 6379:6379
|
|
47
|
+
options: >-
|
|
48
|
+
--health-cmd "redis-cli ping"
|
|
49
|
+
--health-interval 10s
|
|
50
|
+
--health-timeout 5s
|
|
51
|
+
--health-retries 5
|
|
52
|
+
steps:
|
|
53
|
+
- name: Checkout code
|
|
54
|
+
uses: actions/checkout@v5
|
|
55
|
+
- name: Setup Ruby
|
|
56
|
+
uses: ruby/setup-ruby@v1
|
|
57
|
+
with:
|
|
58
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
59
|
+
working-directory: rollout-redis-adapter
|
|
60
|
+
bundler-cache: true
|
|
61
|
+
- name: Run Redis adapter tests
|
|
62
|
+
working-directory: rollout-redis-adapter
|
|
63
|
+
run: |
|
|
64
|
+
mkdir -p test_results
|
|
65
|
+
bundle exec rspec --format progress --format RspecJunitFormatter --out test_results/rspec.xml
|
|
66
|
+
- name: Upload test results
|
|
67
|
+
if: always()
|
|
68
|
+
uses: actions/upload-artifact@v6
|
|
69
|
+
with:
|
|
70
|
+
name: redis-test-results-${{ matrix.ruby-version }}
|
|
71
|
+
path: rollout-redis-adapter/test_results/rspec.xml
|
|
72
|
+
retention-days: 30
|
|
73
|
+
|
|
74
|
+
active_record:
|
|
13
75
|
runs-on: ubuntu-latest
|
|
76
|
+
strategy:
|
|
77
|
+
fail-fast: false
|
|
78
|
+
matrix:
|
|
79
|
+
include:
|
|
80
|
+
- ruby-version: '3.3'
|
|
81
|
+
activerecord: '~> 8.1.0'
|
|
82
|
+
db: sqlite3
|
|
83
|
+
- ruby-version: '3.3'
|
|
84
|
+
activerecord: '~> 8.1.0'
|
|
85
|
+
db: postgresql
|
|
86
|
+
- ruby-version: '3.3'
|
|
87
|
+
activerecord: '~> 8.1.0'
|
|
88
|
+
db: mysql2
|
|
89
|
+
- ruby-version: '3.3'
|
|
90
|
+
activerecord: '~> 7.1.0'
|
|
91
|
+
db: postgresql
|
|
92
|
+
- ruby-version: '3.3'
|
|
93
|
+
activerecord: '~> 7.1.0'
|
|
94
|
+
db: mysql2
|
|
95
|
+
- ruby-version: '3.2'
|
|
96
|
+
activerecord: '~> 8.0.0'
|
|
97
|
+
db: sqlite3
|
|
98
|
+
- ruby-version: '3.1'
|
|
99
|
+
activerecord: '~> 7.2.0'
|
|
100
|
+
db: sqlite3
|
|
101
|
+
- ruby-version: '2.7'
|
|
102
|
+
activerecord: '~> 7.1.0'
|
|
103
|
+
db: sqlite3
|
|
104
|
+
env:
|
|
105
|
+
ACTIVERECORD_VERSION: ${{ matrix.activerecord }}
|
|
106
|
+
ROLLOUT_AR_ADAPTER: ${{ matrix.db }}
|
|
107
|
+
POSTGRES_HOST: localhost
|
|
108
|
+
POSTGRES_USER: postgres
|
|
109
|
+
POSTGRES_PASSWORD: postgres
|
|
110
|
+
POSTGRES_DB: rollout_test
|
|
111
|
+
MYSQL_HOST: 127.0.0.1
|
|
112
|
+
MYSQL_USER: root
|
|
113
|
+
MYSQL_PASSWORD: root
|
|
114
|
+
MYSQL_DATABASE: rollout_test
|
|
115
|
+
REDIS_HOST: 127.0.0.1
|
|
14
116
|
services:
|
|
15
117
|
redis:
|
|
16
118
|
image: redis:7-alpine
|
|
@@ -21,12 +123,52 @@ jobs:
|
|
|
21
123
|
--health-interval 10s
|
|
22
124
|
--health-timeout 5s
|
|
23
125
|
--health-retries 5
|
|
126
|
+
postgres:
|
|
127
|
+
image: postgres:16
|
|
128
|
+
env:
|
|
129
|
+
POSTGRES_USER: postgres
|
|
130
|
+
POSTGRES_PASSWORD: postgres
|
|
131
|
+
POSTGRES_DB: rollout_test
|
|
132
|
+
ports:
|
|
133
|
+
- 5432:5432
|
|
134
|
+
options: >-
|
|
135
|
+
--health-cmd "pg_isready -U postgres"
|
|
136
|
+
--health-interval 10s
|
|
137
|
+
--health-timeout 5s
|
|
138
|
+
--health-retries 5
|
|
139
|
+
mysql:
|
|
140
|
+
image: mysql:8.0
|
|
141
|
+
env:
|
|
142
|
+
MYSQL_ROOT_PASSWORD: root
|
|
143
|
+
MYSQL_DATABASE: rollout_test
|
|
144
|
+
ports:
|
|
145
|
+
- 3306:3306
|
|
146
|
+
options: >-
|
|
147
|
+
--health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -proot"
|
|
148
|
+
--health-interval 10s
|
|
149
|
+
--health-timeout 5s
|
|
150
|
+
--health-retries 5
|
|
24
151
|
steps:
|
|
25
152
|
- name: Checkout code
|
|
26
|
-
uses: actions/checkout@
|
|
153
|
+
uses: actions/checkout@v5
|
|
154
|
+
- name: Install database client libraries
|
|
155
|
+
run: sudo apt-get update && sudo apt-get install -y libpq-dev default-libmysqlclient-dev
|
|
27
156
|
- name: Setup Ruby
|
|
28
157
|
uses: ruby/setup-ruby@v1
|
|
29
158
|
with:
|
|
159
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
160
|
+
working-directory: rollout-active_record-adapter
|
|
30
161
|
bundler-cache: true
|
|
31
|
-
|
|
32
|
-
|
|
162
|
+
cache-version: ar-${{ matrix.ruby-version }}-${{ matrix.activerecord }}-${{ matrix.db }}
|
|
163
|
+
- name: Run Active Record adapter tests
|
|
164
|
+
working-directory: rollout-active_record-adapter
|
|
165
|
+
run: |
|
|
166
|
+
mkdir -p test_results
|
|
167
|
+
bundle exec rspec --format progress --format RspecJunitFormatter --out test_results/rspec.xml
|
|
168
|
+
- name: Upload test results
|
|
169
|
+
if: always()
|
|
170
|
+
uses: actions/upload-artifact@v5
|
|
171
|
+
with:
|
|
172
|
+
name: active-record-test-results-${{ matrix.ruby-version }}-${{ matrix.db }}-${{ strategy.job-index }}
|
|
173
|
+
path: rollout-active_record-adapter/test_results/rspec.xml
|
|
174
|
+
retention-days: 30
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -1,34 +1,43 @@
|
|
|
1
1
|
# rollout
|
|
2
2
|
|
|
3
|
-
Fast feature flags
|
|
3
|
+
Fast feature flags.
|
|
4
|
+
|
|
5
|
+
Upgrading from Rollout 2? Follow the [Rollout 3 upgrade guide](docs/upgrading-to-v3.md)
|
|
6
|
+
before updating your dependencies.
|
|
4
7
|
|
|
5
8
|
[](https://badge.fury.io/rb/rollout)
|
|
6
|
-
[](https://github.com/fetlife/rollout/actions/workflows/test.yml)
|
|
7
10
|
[](https://codeclimate.com/github/FetLife/rollout)
|
|
8
11
|
[](https://codeclimate.com/github/FetLife/rollout/coverage)
|
|
9
12
|
|
|
10
13
|
## Install it
|
|
11
14
|
|
|
12
15
|
```bash
|
|
13
|
-
gem install rollout
|
|
16
|
+
gem install rollout -v '~> 3.1'
|
|
17
|
+
gem install rollout-redis-adapter -v '~> 0.1'
|
|
14
18
|
```
|
|
15
19
|
|
|
20
|
+
```ruby
|
|
21
|
+
gem "rollout", "~> 3.1"
|
|
22
|
+
gem "rollout-redis-adapter", "~> 0.1"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Active Record applications can use `rollout-active_record-adapter` instead of
|
|
26
|
+
`rollout-redis-adapter`. See the [Active Record adapter README](rollout-active_record-adapter/README.md).
|
|
27
|
+
To copy current feature state from Redis, see
|
|
28
|
+
[Migrate from Redis](rollout-active_record-adapter/README.md#migrate-from-redis).
|
|
29
|
+
|
|
16
30
|
## How it works
|
|
17
31
|
|
|
18
32
|
Initialize a rollout object. I assign it to a global var.
|
|
19
33
|
|
|
20
34
|
```ruby
|
|
21
|
-
require
|
|
35
|
+
require "redis"
|
|
36
|
+
require "rollout"
|
|
37
|
+
require "rollout-redis-adapter"
|
|
22
38
|
|
|
23
39
|
$redis = Redis.new
|
|
24
|
-
$rollout = Rollout.new($redis)
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
or even simpler
|
|
28
|
-
|
|
29
|
-
```ruby
|
|
30
|
-
require 'redis'
|
|
31
|
-
$rollout = Rollout.new($redis) # Will use REDIS_URL env var or default redis url
|
|
40
|
+
$rollout = Rollout.new(adapter: Rollout::Adapters::Redis.new($redis))
|
|
32
41
|
```
|
|
33
42
|
|
|
34
43
|
|
|
@@ -109,11 +118,12 @@ $rollout.activate_percentage(:chat, 20)
|
|
|
109
118
|
The algorithm for determining which users get let in is this:
|
|
110
119
|
|
|
111
120
|
```ruby
|
|
112
|
-
|
|
121
|
+
Zlib.crc32(user.id.to_s) < (2**32 - 1) / 100.0 * percentage
|
|
113
122
|
```
|
|
114
123
|
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
The result is deterministic: the same user is always in or out at a given
|
|
125
|
+
percentage, and users already included remain included as the percentage
|
|
126
|
+
increases.
|
|
117
127
|
|
|
118
128
|
Deactivate all percentages like this:
|
|
119
129
|
|
|
@@ -128,7 +138,10 @@ In some cases you might want to have a feature activated for a random set of
|
|
|
128
138
|
users. It can come specially handy when using Rollout for split tests.
|
|
129
139
|
|
|
130
140
|
```ruby
|
|
131
|
-
$rollout = Rollout.new(
|
|
141
|
+
$rollout = Rollout.new(
|
|
142
|
+
adapter: Rollout::Adapters::Redis.new($redis),
|
|
143
|
+
randomize_percentage: true,
|
|
144
|
+
)
|
|
132
145
|
```
|
|
133
146
|
|
|
134
147
|
When on `randomize_percentage` will make sure that 50% of users for feature A
|
|
@@ -166,8 +179,9 @@ failure detection code.
|
|
|
166
179
|
You can inspect the state of your feature using:
|
|
167
180
|
|
|
168
181
|
```ruby
|
|
169
|
-
|
|
170
|
-
|
|
182
|
+
feature = $rollout.get(:chat)
|
|
183
|
+
feature.to_hash
|
|
184
|
+
# => { percentage: 5.0, groups: [:caretakers], users: ["1"], data: {} }
|
|
171
185
|
```
|
|
172
186
|
|
|
173
187
|
## Namespacing
|
|
@@ -180,18 +194,32 @@ environments by using the
|
|
|
180
194
|
[redis-namespace](https://github.com/resque/redis-namespace) gem.
|
|
181
195
|
|
|
182
196
|
```ruby
|
|
197
|
+
gem "redis-namespace"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
require "redis"
|
|
202
|
+
require "redis/namespace"
|
|
203
|
+
require "rollout"
|
|
204
|
+
require "rollout-redis-adapter"
|
|
205
|
+
|
|
183
206
|
$ns = Redis::Namespace.new(Rails.env, redis: $redis)
|
|
184
|
-
$rollout = Rollout.new($ns)
|
|
207
|
+
$rollout = Rollout.new(adapter: Rollout::Adapters::Redis.new($ns))
|
|
185
208
|
$rollout.activate_group(:chat, :all)
|
|
186
209
|
```
|
|
187
210
|
|
|
188
|
-
This example
|
|
211
|
+
This example stores the chat feature at `development:feature:chat` when
|
|
212
|
+
`Rails.env` is `"development"`.
|
|
189
213
|
|
|
190
214
|
## Frontend / UI
|
|
191
215
|
|
|
192
216
|
* [rollout-ui](https://github.com/fetlife/rollout-ui)
|
|
193
217
|
* [Rollout-Dashboard](https://github.com/fiverr/rollout_dashboard/)
|
|
194
218
|
|
|
219
|
+
These integrations may not yet support Rollout 3. Use a version compatible with
|
|
220
|
+
the Rollout release you install. If you depend on rollout-ui, wait for a
|
|
221
|
+
Rollout 3-compatible UI release before upgrading production.
|
|
222
|
+
|
|
195
223
|
## Implementations in other languages
|
|
196
224
|
|
|
197
225
|
* Python: https://github.com/asenchi/proclaim
|
|
@@ -207,13 +235,65 @@ This example would use the "development:feature:chat:groups" key.
|
|
|
207
235
|
* Eric Rafaloff - Maintainer - https://github.com/EricR
|
|
208
236
|
|
|
209
237
|
|
|
210
|
-
##
|
|
238
|
+
## Testing
|
|
239
|
+
|
|
240
|
+
Install dependencies first. Core and the adapter gems have separate Gemfiles:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
bundle install
|
|
244
|
+
bundle install --gemfile=rollout-redis-adapter/Gemfile
|
|
245
|
+
bundle install --gemfile=rollout-active_record-adapter/Gemfile
|
|
246
|
+
```
|
|
211
247
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
248
|
+
Core tests do not need Redis:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
bundle exec rake spec
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Redis adapter tests flush database 7 by default. Use a disposable instance,
|
|
255
|
+
not a shared or production Redis. Start Redis in a separate terminal:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
docker run --rm -p 6379:6379 redis:7-alpine
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Then run:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
bundle exec rake spec:redis
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Optional connection settings: `REDIS_HOST`, `REDIS_PORT`, `REDIS_DB`. `REDIS_DB`
|
|
268
|
+
overrides the default database 7 that is flushed before each example.
|
|
269
|
+
|
|
270
|
+
Active Record adapter tests default to SQLite. PostgreSQL and MySQL are also
|
|
271
|
+
supported:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
bundle exec rake spec:active_record
|
|
275
|
+
ROLLOUT_AR_ADAPTER=postgresql bundle exec rake spec:active_record
|
|
276
|
+
ROLLOUT_AR_ADAPTER=mysql2 bundle exec rake spec:active_record
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Releasing
|
|
215
280
|
|
|
216
|
-
|
|
281
|
+
Each gem has its own version and tag.
|
|
282
|
+
|
|
283
|
+
- Configure a RubyGems trusted publisher for the gem you are releasing. Use
|
|
284
|
+
repository owner `fetlife`, repository `rollout`, workflow filename
|
|
285
|
+
`release.yml`, and no GitHub environment.
|
|
286
|
+
- Update and commit the version in `lib/rollout/version.rb`,
|
|
287
|
+
`rollout-redis-adapter/rollout-redis-adapter.gemspec`, or
|
|
288
|
+
`rollout-active_record-adapter/rollout-active_record-adapter.gemspec`.
|
|
289
|
+
- Tag the release commit with `rollout/vX.Y.Z`,
|
|
290
|
+
`rollout-redis-adapter/vX.Y.Z`, or
|
|
291
|
+
`rollout-active_record-adapter/vX.Y.Z`, matching the gem version.
|
|
292
|
+
- Push the tag with `git push origin <tag>`. CI publishes the selected gem and
|
|
293
|
+
creates its GitHub release.
|
|
294
|
+
|
|
295
|
+
Use package-prefixed tags, not `vX.Y.Z`. Publish core first when the adapter
|
|
296
|
+
depends on a new core version.
|
|
217
297
|
|
|
218
298
|
## Copyright
|
|
219
299
|
|
data/Rakefile
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "bundler/gem_tasks"
|
|
4
3
|
require "rspec/core/rake_task"
|
|
5
4
|
|
|
6
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
5
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
|
6
|
+
task.pattern = "spec/**/*_spec.rb"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
namespace :spec do
|
|
10
|
+
desc "Run Redis adapter tests"
|
|
11
|
+
task :redis do
|
|
12
|
+
gemfile = File.expand_path("rollout-redis-adapter/Gemfile", __dir__)
|
|
13
|
+
Dir.chdir("rollout-redis-adapter") do
|
|
14
|
+
Bundler.with_unbundled_env do
|
|
15
|
+
sh({ "BUNDLE_GEMFILE" => gemfile }, "bundle exec rspec")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "Run Active Record adapter tests"
|
|
21
|
+
task :active_record do
|
|
22
|
+
gemfile = File.expand_path("rollout-active_record-adapter/Gemfile", __dir__)
|
|
23
|
+
Dir.chdir("rollout-active_record-adapter") do
|
|
24
|
+
Bundler.with_unbundled_env do
|
|
25
|
+
sh({ "BUNDLE_GEMFILE" => gemfile }, "bundle exec rspec")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
7
30
|
|
|
8
31
|
task default: :spec
|
|
9
32
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Upgrading to Rollout 3
|
|
2
|
+
|
|
3
|
+
Rollout 3 keeps feature evaluation in the `rollout` gem and moves Redis
|
|
4
|
+
persistence to `rollout-redis-adapter`. Existing Redis keys stay in place. This is
|
|
5
|
+
not a storage migration onto Active Record. For the Active Record adapter
|
|
6
|
+
and Redis cutover, see
|
|
7
|
+
[rollout-active_record-adapter/README.md](../rollout-active_record-adapter/README.md).
|
|
8
|
+
|
|
9
|
+
## 1. Update dependencies
|
|
10
|
+
|
|
11
|
+
Add the adapter gem next to `rollout`:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "rollout", "~> 3.1"
|
|
15
|
+
gem "rollout-redis-adapter", "~> 0.1"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Use compatible releases of both gems. The Redis adapter is versioned
|
|
19
|
+
independently of core. Rollout `3.0.0` used `backend:` and
|
|
20
|
+
`Rollout::Redis::Backend`; `3.1.0` uses `adapter:` and
|
|
21
|
+
`Rollout::Adapters::Redis`. If you use
|
|
22
|
+
[rollout-ui](https://github.com/fetlife/rollout-ui), wait for a Rollout
|
|
23
|
+
3-compatible UI release before upgrading production.
|
|
24
|
+
|
|
25
|
+
## 2. Update requires and initialization
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Before
|
|
29
|
+
require "redis"
|
|
30
|
+
require "rollout"
|
|
31
|
+
|
|
32
|
+
$redis = Redis.new
|
|
33
|
+
$rollout = Rollout.new(
|
|
34
|
+
$redis,
|
|
35
|
+
randomize_percentage: true,
|
|
36
|
+
logging: { history_length: 100, global: true },
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# After
|
|
42
|
+
require "redis"
|
|
43
|
+
require "rollout"
|
|
44
|
+
require "rollout-redis-adapter"
|
|
45
|
+
|
|
46
|
+
$redis = Redis.new
|
|
47
|
+
$rollout = Rollout.new(
|
|
48
|
+
adapter: Rollout::Adapters::Redis.new($redis),
|
|
49
|
+
randomize_percentage: true,
|
|
50
|
+
logging: { history_length: 100, global: true },
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Pass the same Redis client, database, and namespace you already use. Repeat
|
|
55
|
+
this change at every initialization site, including jobs, scripts, and
|
|
56
|
+
consoles.
|
|
57
|
+
|
|
58
|
+
`redis-namespace` still works:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
$ns = Redis::Namespace.new(Rails.env, redis: $redis)
|
|
62
|
+
$rollout = Rollout.new(adapter: Rollout::Adapters::Redis.new($ns))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 3. Breaking changes
|
|
66
|
+
|
|
67
|
+
| Area | Breaking change | Required action |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| Storage configuration | `Rollout.new(redis, options)` now requires `adapter:`. | Wrap the existing client in `Rollout::Adapters::Redis.new(redis)` and pass options as keywords. |
|
|
70
|
+
| Storage access | `rollout.storage` is removed. `rollout.adapter` returns an adapter, not the Redis client. | Keep your own Redis client reference if application code needs direct access. |
|
|
71
|
+
| Logging storage | `logging: { storage: other_redis }` is no longer supported. The Redis backend stores features and history through the same client. | Applications using separate history storage cannot preserve that setup with the current adapter. Removing the option does not migrate existing history. |
|
|
72
|
+
| Custom logging | `Logger#log` and `Logger#update` are removed. Built-in logging is no longer an observer. | Use Rollout mutations and `logging.with_context` rather than calling those methods directly. |
|
|
73
|
+
| Feature construction | `Feature.new(name, rollout:, state: payload)` no longer accepts a name argument or raw Redis payload. | Prefer `rollout.get(name)`. Direct construction requires `Feature.new(state: feature_state, rollout: rollout, options: rollout.options)`. |
|
|
74
|
+
| Feature serialization | `feature.serialize` is removed. | Use `feature.to_feature_state` for a backend-neutral snapshot. Redis encoding belongs to `Rollout::Redis::Codec`. |
|
|
75
|
+
| Feature metadata | Metadata is canonicalized to JSON. Symbol keys and values become strings. Values such as `Time`, `Date`, and `BigDecimal` persist as their JSON representations, not as original Ruby objects. | No data migration is required. JSON-serializable writes continue to work. |
|
|
76
|
+
| History decoding | `Logging::Event.from_raw` is removed. | Decode persisted Redis members with `Rollout::Redis::Codec.decode_event(value, score)`. |
|
|
77
|
+
|
|
78
|
+
## 4. History and lifecycle
|
|
79
|
+
|
|
80
|
+
History reads can request a bound. `limit` is the newest N events, returned
|
|
81
|
+
oldest-to-newest. Omit `limit` to read the full retained history. `0` returns
|
|
82
|
+
no events.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
rollout.logging.events(:chat, limit: 10)
|
|
86
|
+
rollout.logging.global_events(limit: 10)
|
|
87
|
+
rollout.logging.last_event(:chat)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`last_event` reads one persisted member. It does not load the complete
|
|
91
|
+
history.
|
|
92
|
+
|
|
93
|
+
| Operation | Feature state | Per-feature history | Global history |
|
|
94
|
+
| --- | --- | --- | --- |
|
|
95
|
+
| `delete`, logging enabled | Removed | Removed | Retained; emits a global `delete` event when `global: true` and the adapter supports event-aware deletion |
|
|
96
|
+
| `delete`, logging disabled | Removed | Retained | Retained |
|
|
97
|
+
| `logging.delete` | Unchanged | Removed | Retained |
|
|
98
|
+
| `clear!` | Removed | Retained | Retained |
|
|
99
|
+
|
|
100
|
+
The deletion event includes the feature name, timestamp, and current logging
|
|
101
|
+
context (for example, an actor supplied with `logging.with_context`). Built-in
|
|
102
|
+
adapters write it only when the feature exists and global logging is enabled;
|
|
103
|
+
it is subject to `history_length` like other global events. Older or custom
|
|
104
|
+
adapters without `delete_feature_with_history` retain the previous deletion
|
|
105
|
+
behavior and do not record a deletion event.
|
|
106
|
+
|
|
107
|
+
`clear!` still resets each feature first, so logging-enabled instances record
|
|
108
|
+
a reset event before the state is deleted. History remains subject to
|
|
109
|
+
`history_length`. The Redis registry key is removed after clearing, including
|
|
110
|
+
when it was already empty.
|