flipper 1.3.6 → 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/.github/workflows/ci.yml +8 -5
- data/.github/workflows/examples.yml +4 -3
- data/.github/workflows/release.yml +54 -0
- data/CLAUDE.md +16 -3
- data/README.md +4 -3
- data/examples/cloud/poll_interval/README.md +111 -0
- data/examples/cloud/poll_interval/client.rb +108 -0
- data/examples/cloud/poll_interval/server.rb +98 -0
- data/examples/expressions.rb +35 -11
- data/lib/flipper/adapter.rb +17 -1
- data/lib/flipper/adapters/actor_limit.rb +27 -1
- data/lib/flipper/adapters/cache_base.rb +21 -3
- data/lib/flipper/adapters/dual_write.rb +6 -2
- data/lib/flipper/adapters/failover.rb +9 -3
- data/lib/flipper/adapters/failsafe.rb +2 -2
- data/lib/flipper/adapters/http/client.rb +15 -4
- data/lib/flipper/adapters/http.rb +37 -2
- data/lib/flipper/adapters/instrumented.rb +2 -2
- data/lib/flipper/adapters/memoizable.rb +3 -3
- data/lib/flipper/adapters/memory.rb +1 -1
- data/lib/flipper/adapters/pstore.rb +1 -1
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +5 -1
- data/lib/flipper/adapters/sync/synchronizer.rb +10 -5
- data/lib/flipper/adapters/sync.rb +7 -3
- data/lib/flipper/cli.rb +51 -0
- data/lib/flipper/cloud/configuration.rb +9 -4
- data/lib/flipper/cloud/dsl.rb +2 -2
- data/lib/flipper/cloud/middleware.rb +1 -1
- data/lib/flipper/cloud/migrate.rb +71 -0
- data/lib/flipper/cloud/telemetry.rb +1 -1
- data/lib/flipper/cloud.rb +1 -0
- data/lib/flipper/dsl.rb +1 -1
- data/lib/flipper/expressions/feature_enabled.rb +34 -0
- data/lib/flipper/expressions/time.rb +8 -1
- data/lib/flipper/gates/expression.rb +2 -2
- data/lib/flipper/poller.rb +47 -8
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +17 -1
- data/spec/flipper/adapter_spec.rb +20 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +55 -0
- data/spec/flipper/adapters/dual_write_spec.rb +13 -0
- data/spec/flipper/adapters/failover_spec.rb +12 -0
- data/spec/flipper/adapters/http_spec.rb +151 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +12 -0
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
- data/spec/flipper/adapters/sync_spec.rb +13 -0
- data/spec/flipper/cli_spec.rb +51 -0
- data/spec/flipper/cloud/configuration_spec.rb +6 -0
- data/spec/flipper/cloud/dsl_spec.rb +10 -2
- data/spec/flipper/cloud/middleware_spec.rb +34 -16
- data/spec/flipper/cloud/migrate_spec.rb +160 -0
- data/spec/flipper/cloud/telemetry_spec.rb +1 -1
- data/spec/flipper/engine_spec.rb +2 -2
- data/spec/flipper/expressions/time_spec.rb +16 -0
- data/spec/flipper/gates/expression_spec.rb +82 -0
- data/spec/flipper/middleware/memoizer_spec.rb +37 -6
- data/spec/flipper/poller_spec.rb +347 -4
- data/spec/flipper_integration_spec.rb +133 -0
- data/spec/flipper_spec.rb +6 -1
- metadata +16 -112
- data/lib/flipper/expressions/duration.rb +0 -28
- data/spec/flipper/expressions/duration_spec.rb +0 -43
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 881ca599099f433a4204f2a058c512b6b9198f529d1db4fd3a3445ba822dcd9b
|
|
4
|
+
data.tar.gz: 5ad5da793cec928e09357f2d46ddb139c159dfc1d5b9ddea4ae4c0c52193a75b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b049bcd83af70810efd36f1873fb9d72cf22f95cf460d80829b7ad7ed68b7ef11e619b621ea001c230dfdbee43c453e76326de59be2306ee52b23444b50619b
|
|
7
|
+
data.tar.gz: fb82eca0e34233cf3fe9e001923f3b87ae2acf9f7dfdbbdce553d1b8408d5f23df075d789f5be2f975d6facf077637294f306d774e026005e4a7a13124866a3a
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
name: CI
|
|
2
|
-
on: [push
|
|
2
|
+
on: [push]
|
|
3
|
+
|
|
3
4
|
jobs:
|
|
4
5
|
test:
|
|
5
6
|
name: Test on ruby ${{ matrix.ruby }} and rails ${{ matrix.rails }}
|
|
6
7
|
runs-on: ubuntu-latest
|
|
8
|
+
timeout-minutes: 20
|
|
7
9
|
services:
|
|
8
10
|
redis:
|
|
9
11
|
image: redis
|
|
@@ -13,6 +15,9 @@ jobs:
|
|
|
13
15
|
--health-interval 10s
|
|
14
16
|
--health-timeout 5s
|
|
15
17
|
--health-retries 5
|
|
18
|
+
memcached:
|
|
19
|
+
image: memcached
|
|
20
|
+
ports: ["11211:11211"]
|
|
16
21
|
postgres:
|
|
17
22
|
image: postgres:13
|
|
18
23
|
ports:
|
|
@@ -81,14 +86,12 @@ jobs:
|
|
|
81
86
|
steps:
|
|
82
87
|
- name: Set up MySQL
|
|
83
88
|
run: sudo /etc/init.d/mysql start
|
|
84
|
-
- name: Setup memcached
|
|
85
|
-
uses: KeisukeYamashita/memcached-actions@v1
|
|
86
89
|
- name: Start MongoDB
|
|
87
|
-
uses: supercharge/mongodb-github-action@1.12.
|
|
90
|
+
uses: supercharge/mongodb-github-action@1.12.1
|
|
88
91
|
with:
|
|
89
92
|
mongodb-version: 4.0
|
|
90
93
|
- name: Check out repository code
|
|
91
|
-
uses: actions/checkout@
|
|
94
|
+
uses: actions/checkout@v6
|
|
92
95
|
- name: Do some action caching
|
|
93
96
|
uses: actions/cache@v4
|
|
94
97
|
with:
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
name: Examples
|
|
2
|
-
on: [push
|
|
2
|
+
on: [push]
|
|
3
3
|
jobs:
|
|
4
4
|
test:
|
|
5
5
|
if: github.repository_owner == 'flippercloud'
|
|
6
6
|
name: Example on ruby ${{ matrix.ruby }} and rails ${{ matrix.rails }}
|
|
7
7
|
runs-on: ubuntu-latest
|
|
8
|
+
timeout-minutes: 20
|
|
8
9
|
services:
|
|
9
10
|
redis:
|
|
10
11
|
image: redis
|
|
@@ -66,11 +67,11 @@ jobs:
|
|
|
66
67
|
- name: Setup memcached
|
|
67
68
|
uses: KeisukeYamashita/memcached-actions@v1
|
|
68
69
|
- name: Start MongoDB
|
|
69
|
-
uses: supercharge/mongodb-github-action@1.12.
|
|
70
|
+
uses: supercharge/mongodb-github-action@1.12.1
|
|
70
71
|
with:
|
|
71
72
|
mongodb-version: 4.0
|
|
72
73
|
- name: Check out repository code
|
|
73
|
-
uses: actions/checkout@
|
|
74
|
+
uses: actions/checkout@v6
|
|
74
75
|
- name: Do some action caching
|
|
75
76
|
uses: actions/cache@v4
|
|
76
77
|
with:
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # RubyGems trusted publishing (OIDC)
|
|
13
|
+
contents: write # Create GitHub Releases
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
|
+
|
|
17
|
+
- uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: '3.3'
|
|
20
|
+
|
|
21
|
+
- name: Build all gems
|
|
22
|
+
run: |
|
|
23
|
+
for gemspec in *.gemspec; do
|
|
24
|
+
gem build "$gemspec"
|
|
25
|
+
done
|
|
26
|
+
mkdir -p pkg
|
|
27
|
+
mv *.gem pkg/
|
|
28
|
+
echo "Built gems:"
|
|
29
|
+
ls -la pkg/
|
|
30
|
+
|
|
31
|
+
- name: Configure RubyGems credentials
|
|
32
|
+
uses: rubygems/configure-rubygems-credentials@v1.0.0
|
|
33
|
+
|
|
34
|
+
- name: Publish all gems
|
|
35
|
+
run: |
|
|
36
|
+
# Push core gem first since other gems depend on it
|
|
37
|
+
echo "Pushing flipper core gem..."
|
|
38
|
+
gem push pkg/flipper-[0-9]*.gem || echo "Core gem already pushed, continuing..."
|
|
39
|
+
|
|
40
|
+
for gem_file in pkg/flipper-*.gem; do
|
|
41
|
+
basename="$(basename "$gem_file")"
|
|
42
|
+
if [[ "$basename" =~ ^flipper-[0-9] ]]; then
|
|
43
|
+
continue
|
|
44
|
+
fi
|
|
45
|
+
echo "Pushing $basename..."
|
|
46
|
+
gem push "$gem_file" || echo "Failed to push $basename, continuing..."
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
- name: Create draft GitHub Release
|
|
50
|
+
uses: softprops/action-gh-release@v2
|
|
51
|
+
with:
|
|
52
|
+
draft: true
|
|
53
|
+
generate_release_notes: true
|
|
54
|
+
files: pkg/*.gem
|
data/CLAUDE.md
CHANGED
|
@@ -17,9 +17,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
17
17
|
- `script/console` - Start interactive console with Flipper loaded (uses Pry)
|
|
18
18
|
- `script/server` - Start local UI server on port 9999 for testing web interface
|
|
19
19
|
|
|
20
|
-
###
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
### Releasing
|
|
21
|
+
1. Bump version in `lib/flipper/version.rb`, commit, and push to main
|
|
22
|
+
2. Tag and push: `git tag v1.x.x && git push origin v1.x.x`
|
|
23
|
+
3. GitHub Actions (`.github/workflows/release.yml`) builds and publishes all 12 gems via RubyGems trusted publishing, then creates a draft GitHub Release
|
|
24
|
+
4. Edit and publish the draft release at https://github.com/flippercloud/flipper/releases
|
|
25
|
+
|
|
26
|
+
- `bundle exec rake build` - Build all gems locally into pkg/ directory
|
|
27
|
+
- `script/release` - Manual fallback for local releases (prompts for OTP)
|
|
23
28
|
|
|
24
29
|
## Architecture Overview
|
|
25
30
|
|
|
@@ -69,6 +74,14 @@ The project is structured as multiple gems:
|
|
|
69
74
|
**Memoization**: Automatic caching of feature checks within request/thread scope
|
|
70
75
|
**Type Safety**: Strong typing system for actors, percentages, and other values
|
|
71
76
|
|
|
77
|
+
### Serialization and HTTP
|
|
78
|
+
|
|
79
|
+
Use `Flipper::Typecast` for JSON and gzip serialization instead of calling `JSON.generate`/`JSON.parse` or `Zlib` directly:
|
|
80
|
+
- `Typecast.to_json(hash)` / `Typecast.from_json(string)` for JSON serialization
|
|
81
|
+
- `Typecast.to_gzip(string)` / `Typecast.from_gzip(string)` for gzip compression
|
|
82
|
+
|
|
83
|
+
For outbound HTTP requests, use `Flipper::Adapters::Http::Client` instead of raw `Net::HTTP`. It provides timeouts, retries (`max_retries`), SSL verification, and diagnostic headers (user-agent, client-language, client-platform, etc.). See `lib/flipper/cloud/migrate.rb` for an example.
|
|
84
|
+
|
|
72
85
|
### Testing
|
|
73
86
|
|
|
74
87
|
Uses both RSpec (currently preferred for new tests) and Minitest. Shared adapter specs ensure consistency across all storage backends. Extensive testing across multiple Rails versions (5.0-8.0).
|
data/README.md
CHANGED
|
@@ -97,9 +97,10 @@ We also have a [free plan](https://www.flippercloud.io?utm_source=oss&utm_medium
|
|
|
97
97
|
|
|
98
98
|
## Releasing
|
|
99
99
|
|
|
100
|
-
1. Update the version
|
|
101
|
-
2. `
|
|
102
|
-
3.
|
|
100
|
+
1. Update the version in `lib/flipper/version.rb` and commit.
|
|
101
|
+
2. Tag and push: `git tag v1.x.x && git push origin v1.x.x`
|
|
102
|
+
3. GitHub Actions builds and publishes all gems to RubyGems automatically.
|
|
103
|
+
4. Edit and publish the draft [GitHub Release](https://github.com/flippercloud/flipper/releases).
|
|
103
104
|
|
|
104
105
|
## Brought To You By
|
|
105
106
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Poll Interval Dynamic Adjustment Demo
|
|
2
|
+
|
|
3
|
+
This demo shows how the Flipper poller dynamically adjusts its polling interval based on the `poll-interval` header from the server, and how it responds to the `poll-shutdown` header.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `server.rb` - Test server that responds with configurable headers
|
|
8
|
+
- `client.rb` - Client that polls the server and logs interval changes
|
|
9
|
+
- `README.md` - This file
|
|
10
|
+
|
|
11
|
+
## How to Run
|
|
12
|
+
|
|
13
|
+
### Terminal 1: Start the Server
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle exec ruby examples/cloud/poll_interval/server.rb
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The server will start on http://localhost:3000 and show a prompt where you can control what headers to send.
|
|
20
|
+
|
|
21
|
+
### Terminal 2: Start the Client
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bundle exec ruby examples/cloud/poll_interval/client.rb
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The client will start polling the server every 10 seconds (the minimum) and log all activity.
|
|
28
|
+
|
|
29
|
+
## Testing Scenarios
|
|
30
|
+
|
|
31
|
+
### 1. Change Poll Interval
|
|
32
|
+
|
|
33
|
+
In the **server terminal**, type a number to set the poll interval:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
> 20
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
In the **client terminal**, you'll see:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
[HH:MM:SS] WARN: ⚠️ INTERVAL CHANGED: 10.0s → 20.0s
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The client will now poll every 20 seconds instead of 10.
|
|
46
|
+
|
|
47
|
+
### 2. Try an Invalid Interval (Below Minimum)
|
|
48
|
+
|
|
49
|
+
In the **server terminal**:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
> 5
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
In the **client terminal**, you'll see a warning:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Flipper::Cloud poll interval must be greater than or equal to 10 but was 5.0. Setting interval to 10.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The interval will remain at 10 seconds (the minimum).
|
|
62
|
+
|
|
63
|
+
### 3. Trigger Shutdown
|
|
64
|
+
|
|
65
|
+
In the **server terminal**:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
> shutdown
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
In the **client terminal**, you'll see:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
[HH:MM:SS] WARN: Shutdown requested by server via poll-shutdown header
|
|
75
|
+
[HH:MM:SS] WARN: Poller stopped
|
|
76
|
+
[HH:MM:SS] WARN: Poller thread is no longer running
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The poller will stop gracefully.
|
|
80
|
+
|
|
81
|
+
### 4. Reset Headers
|
|
82
|
+
|
|
83
|
+
In the **server terminal**:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
> reset
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The server will stop sending special headers. The client will continue with its current interval.
|
|
90
|
+
|
|
91
|
+
## What You'll Learn
|
|
92
|
+
|
|
93
|
+
- How `poll-interval` header dynamically adjusts polling frequency
|
|
94
|
+
- How `poll-shutdown` header gracefully stops the poller
|
|
95
|
+
- How minimum interval enforcement works (10 seconds minimum)
|
|
96
|
+
- How the poller continues working even if the server returns errors
|
|
97
|
+
- Real-time logging of poller events via instrumentation
|
|
98
|
+
|
|
99
|
+
## Implementation Details
|
|
100
|
+
|
|
101
|
+
The poller checks response headers in the `ensure` block of the `sync` method, which means:
|
|
102
|
+
|
|
103
|
+
- Interval adjustments happen even if the sync fails with an error
|
|
104
|
+
- Shutdown signals are never missed, even during failures
|
|
105
|
+
- The poller is resilient to network issues
|
|
106
|
+
|
|
107
|
+
The `interval=` setter handles all validation:
|
|
108
|
+
|
|
109
|
+
- Type conversion via `Flipper::Typecast.to_float`
|
|
110
|
+
- Minimum enforcement (10 seconds)
|
|
111
|
+
- Warning messages for invalid values
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Example showing poll interval being dynamically adjusted via poll-interval header
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# 1. Terminal 1: bundle exec ruby examples/cloud/poll_interval/server.rb
|
|
5
|
+
# 2. Terminal 2: bundle exec ruby examples/cloud/poll_interval/client.rb
|
|
6
|
+
|
|
7
|
+
require 'bundler/setup'
|
|
8
|
+
require 'flipper'
|
|
9
|
+
require 'flipper/adapters/http'
|
|
10
|
+
require 'flipper/poller'
|
|
11
|
+
require 'logger'
|
|
12
|
+
|
|
13
|
+
# Setup logging to show what's happening
|
|
14
|
+
logger = Logger.new(STDOUT)
|
|
15
|
+
logger.level = Logger::INFO
|
|
16
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
|
17
|
+
"[#{datetime.strftime('%H:%M:%S')}] #{severity}: #{msg}\n"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Create HTTP adapter pointing to localhost:3000
|
|
21
|
+
http_adapter = Flipper::Adapters::Http.new(url: 'http://localhost:3000/flipper')
|
|
22
|
+
|
|
23
|
+
# Create instrumenter to log poller events
|
|
24
|
+
instrumenter = Module.new do
|
|
25
|
+
def self.instrument(name, payload = {})
|
|
26
|
+
case payload[:operation]
|
|
27
|
+
when :poll
|
|
28
|
+
logger.info "Polling remote adapter..."
|
|
29
|
+
when :shutdown_requested
|
|
30
|
+
logger.warn "Shutdown requested by server via poll-shutdown header"
|
|
31
|
+
when :stop
|
|
32
|
+
logger.warn "Poller stopped"
|
|
33
|
+
when :thread_start
|
|
34
|
+
logger.info "Poller thread started"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
result = yield if block_given?
|
|
38
|
+
|
|
39
|
+
if payload[:operation] == :poll && result
|
|
40
|
+
logger.info "Poll completed successfully"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.logger=(l)
|
|
47
|
+
@logger = l
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.logger
|
|
51
|
+
@logger
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
instrumenter.logger = logger
|
|
55
|
+
|
|
56
|
+
# Create poller with custom instrumenter and short initial interval
|
|
57
|
+
poller = Flipper::Poller.new(
|
|
58
|
+
remote_adapter: http_adapter,
|
|
59
|
+
interval: 5, # Start with 5 second interval (will be enforced to 10 minimum)
|
|
60
|
+
instrumenter: instrumenter,
|
|
61
|
+
start_automatically: false,
|
|
62
|
+
shutdown_automatically: false
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
logger.info "Starting poller with interval: #{poller.interval} seconds"
|
|
66
|
+
logger.info "Minimum allowed interval: #{Flipper::Poller::MINIMUM_POLL_INTERVAL} seconds"
|
|
67
|
+
logger.info ""
|
|
68
|
+
logger.info "Server can control polling via response headers:"
|
|
69
|
+
logger.info " - poll-interval: <seconds> (adjust poll frequency)"
|
|
70
|
+
logger.info " - poll-shutdown: true (stop polling)"
|
|
71
|
+
logger.info ""
|
|
72
|
+
|
|
73
|
+
# Track interval changes
|
|
74
|
+
last_interval = poller.interval
|
|
75
|
+
|
|
76
|
+
# Start the poller
|
|
77
|
+
poller.start
|
|
78
|
+
|
|
79
|
+
# Monitor for interval changes and log them
|
|
80
|
+
logger.info "Monitoring poller... (Ctrl+C to exit)"
|
|
81
|
+
logger.info ""
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
loop do
|
|
85
|
+
sleep 2
|
|
86
|
+
|
|
87
|
+
current_interval = poller.interval
|
|
88
|
+
|
|
89
|
+
# Highlight when it changes
|
|
90
|
+
if current_interval != last_interval
|
|
91
|
+
logger.warn "⚠️ INTERVAL CHANGED: #{last_interval}s → #{current_interval}s"
|
|
92
|
+
last_interval = current_interval
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if poller thread is still alive
|
|
96
|
+
unless poller.thread&.alive?
|
|
97
|
+
logger.warn "Poller thread is no longer running"
|
|
98
|
+
break
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
rescue Interrupt
|
|
102
|
+
logger.info ""
|
|
103
|
+
logger.info "Interrupted by user"
|
|
104
|
+
ensure
|
|
105
|
+
logger.info "Stopping poller..."
|
|
106
|
+
poller.stop
|
|
107
|
+
logger.info "Final interval: #{poller.interval} seconds"
|
|
108
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Simple test server for demonstrating poll interval changes
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# 1. Terminal 1: bundle exec ruby examples/cloud/poll_interval/server.rb
|
|
5
|
+
# 2. Terminal 2: bundle exec ruby examples/cloud/poll_interval/client.rb
|
|
6
|
+
#
|
|
7
|
+
# Commands in server terminal:
|
|
8
|
+
# - Type a number (e.g., "15") to set poll-interval header to that value
|
|
9
|
+
# - Type "shutdown" to send poll-shutdown: true header
|
|
10
|
+
# - Type "reset" to stop sending special headers
|
|
11
|
+
# - Ctrl+C to exit
|
|
12
|
+
|
|
13
|
+
require 'bundler/setup'
|
|
14
|
+
require 'webrick'
|
|
15
|
+
require 'json'
|
|
16
|
+
|
|
17
|
+
# State for what headers to send
|
|
18
|
+
$poll_interval = nil
|
|
19
|
+
$poll_shutdown = false
|
|
20
|
+
|
|
21
|
+
# Thread to handle user input for changing headers
|
|
22
|
+
input_thread = Thread.new do
|
|
23
|
+
puts ""
|
|
24
|
+
puts "=" * 60
|
|
25
|
+
puts "Server Controls:"
|
|
26
|
+
puts " Type a number (e.g., '15') to set poll-interval"
|
|
27
|
+
puts " Type 'shutdown' to trigger poll shutdown"
|
|
28
|
+
puts " Type 'reset' to clear all special headers"
|
|
29
|
+
puts "=" * 60
|
|
30
|
+
puts ""
|
|
31
|
+
|
|
32
|
+
loop do
|
|
33
|
+
print "> "
|
|
34
|
+
input = gets&.chomp
|
|
35
|
+
break if input.nil?
|
|
36
|
+
|
|
37
|
+
case input
|
|
38
|
+
when /^\d+$/
|
|
39
|
+
$poll_interval = input.to_i
|
|
40
|
+
puts "✓ Will send poll-interval: #{$poll_interval}"
|
|
41
|
+
when "shutdown"
|
|
42
|
+
$poll_shutdown = true
|
|
43
|
+
puts "✓ Will send poll-shutdown: true"
|
|
44
|
+
when "reset"
|
|
45
|
+
$poll_interval = nil
|
|
46
|
+
$poll_shutdown = false
|
|
47
|
+
puts "✓ Cleared all special headers"
|
|
48
|
+
else
|
|
49
|
+
puts "Unknown command. Use a number, 'shutdown', or 'reset'"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Setup WEBrick server
|
|
55
|
+
server = WEBrick::HTTPServer.new(
|
|
56
|
+
Port: 3000,
|
|
57
|
+
Logger: WEBrick::Log.new($stdout, WEBrick::Log::INFO),
|
|
58
|
+
AccessLog: [[
|
|
59
|
+
$stdout,
|
|
60
|
+
WEBrick::AccessLog::COMMON_LOG_FORMAT
|
|
61
|
+
]]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Handle GET /flipper/features
|
|
65
|
+
server.mount_proc '/flipper/features' do |req, res|
|
|
66
|
+
# Build response
|
|
67
|
+
response_body = {
|
|
68
|
+
features: []
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
res.status = 200
|
|
72
|
+
res['Content-Type'] = 'application/json'
|
|
73
|
+
res.body = JSON.generate(response_body)
|
|
74
|
+
|
|
75
|
+
# Add special headers if configured
|
|
76
|
+
if $poll_interval
|
|
77
|
+
res['poll-interval'] = $poll_interval.to_s
|
|
78
|
+
puts "→ Sent poll-interval: #{$poll_interval}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if $poll_shutdown
|
|
82
|
+
res['poll-shutdown'] = 'true'
|
|
83
|
+
puts "→ Sent poll-shutdown: true"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Trap interrupt and shutdown gracefully
|
|
88
|
+
trap('INT') do
|
|
89
|
+
puts "\nShutting down server..."
|
|
90
|
+
server.shutdown
|
|
91
|
+
input_thread.kill
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
puts "Server starting on http://localhost:3000"
|
|
95
|
+
puts "Endpoint: GET http://localhost:3000/flipper/features"
|
|
96
|
+
puts ""
|
|
97
|
+
|
|
98
|
+
server.start
|
data/examples/expressions.rb
CHANGED
|
@@ -31,13 +31,9 @@ class Org < Struct.new(:id, :flipper_properties)
|
|
|
31
31
|
include Flipper::Identifier
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
NOW = Time.now.to_i
|
|
35
|
-
DAY = 60 * 60 * 24
|
|
36
|
-
|
|
37
34
|
org = Org.new(1, {
|
|
38
35
|
"type" => "Org",
|
|
39
36
|
"id" => 1,
|
|
40
|
-
"now" => NOW,
|
|
41
37
|
})
|
|
42
38
|
|
|
43
39
|
user = User.new(1, {
|
|
@@ -46,7 +42,6 @@ user = User.new(1, {
|
|
|
46
42
|
"plan" => "basic",
|
|
47
43
|
"age" => 39,
|
|
48
44
|
"team_user" => true,
|
|
49
|
-
"now" => NOW,
|
|
50
45
|
})
|
|
51
46
|
|
|
52
47
|
admin_user = User.new(2, {
|
|
@@ -54,7 +49,6 @@ admin_user = User.new(2, {
|
|
|
54
49
|
"id" => 2,
|
|
55
50
|
"admin" => true,
|
|
56
51
|
"team_user" => true,
|
|
57
|
-
"now" => NOW,
|
|
58
52
|
})
|
|
59
53
|
|
|
60
54
|
other_user = User.new(3, {
|
|
@@ -63,7 +57,6 @@ other_user = User.new(3, {
|
|
|
63
57
|
"plan" => "plus",
|
|
64
58
|
"age" => 18,
|
|
65
59
|
"org_admin" => true,
|
|
66
|
-
"now" => NOW - DAY,
|
|
67
60
|
})
|
|
68
61
|
|
|
69
62
|
age_expression = Flipper.property(:age).gte(21)
|
|
@@ -205,9 +198,40 @@ assert Flipper.enabled?(:something, user)
|
|
|
205
198
|
assert Flipper.enabled?(:something, admin_user)
|
|
206
199
|
refute Flipper.enabled?(:something, other_user)
|
|
207
200
|
|
|
208
|
-
puts "\n\nEnabling based on time"
|
|
209
|
-
|
|
210
|
-
Flipper.enable :something,
|
|
201
|
+
puts "\n\nEnabling based on time (epoch)"
|
|
202
|
+
scheduled_epoch = Flipper.now.gte(Flipper.time(Time.now.to_i - 86_400))
|
|
203
|
+
Flipper.enable :something, scheduled_epoch
|
|
211
204
|
assert Flipper.enabled?(:something, user)
|
|
212
205
|
assert Flipper.enabled?(:something, admin_user)
|
|
213
|
-
|
|
206
|
+
assert Flipper.enabled?(:something, other_user)
|
|
207
|
+
reset
|
|
208
|
+
|
|
209
|
+
puts "\n\nEnabling based on time (datetime)"
|
|
210
|
+
past_time = (Time.now.utc - 86_400).iso8601
|
|
211
|
+
scheduled_datetime = Flipper.now.gte(Flipper.time(past_time))
|
|
212
|
+
Flipper.enable :something, scheduled_datetime
|
|
213
|
+
assert Flipper.enabled?(:something, user)
|
|
214
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
215
|
+
assert Flipper.enabled?(:something, other_user)
|
|
216
|
+
reset
|
|
217
|
+
|
|
218
|
+
puts "\n\nDisabling after a time (expiring feature)"
|
|
219
|
+
future_time = (Time.now.utc + 86_400).iso8601
|
|
220
|
+
expiring_expression = Flipper.now.lt(Flipper.time(future_time))
|
|
221
|
+
Flipper.enable :something, expiring_expression
|
|
222
|
+
assert Flipper.enabled?(:something, user)
|
|
223
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
224
|
+
assert Flipper.enabled?(:something, other_user)
|
|
225
|
+
reset
|
|
226
|
+
|
|
227
|
+
puts "\n\nEnabling within a time window"
|
|
228
|
+
start_time = (Time.now.utc - 86_400).iso8601
|
|
229
|
+
end_time = (Time.now.utc + 86_400).iso8601
|
|
230
|
+
time_window = Flipper.all(
|
|
231
|
+
Flipper.now.gte(Flipper.time(start_time)),
|
|
232
|
+
Flipper.now.lt(Flipper.time(end_time))
|
|
233
|
+
)
|
|
234
|
+
Flipper.enable :something, time_window
|
|
235
|
+
assert Flipper.enabled?(:something, user)
|
|
236
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
237
|
+
assert Flipper.enabled?(:something, other_user)
|
data/lib/flipper/adapter.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Flipper
|
|
|
31
31
|
# Public: Get all features and gate values in one call. Defaults to one call
|
|
32
32
|
# to features and another to get_multi. Feel free to override per adapter to
|
|
33
33
|
# make this more efficient.
|
|
34
|
-
def get_all
|
|
34
|
+
def get_all(**kwargs)
|
|
35
35
|
instances = features.map { |key| Flipper::Feature.new(key, self) }
|
|
36
36
|
get_multi(instances)
|
|
37
37
|
end
|
|
@@ -73,6 +73,22 @@ module Flipper
|
|
|
73
73
|
def name
|
|
74
74
|
@name ||= self.class.name.split('::').last.split(/(?=[A-Z])/).join('_').downcase.to_sym
|
|
75
75
|
end
|
|
76
|
+
|
|
77
|
+
# Public: Returns a string representation of the adapter stack for debugging.
|
|
78
|
+
# Shows the full chain of wrapped adapters.
|
|
79
|
+
#
|
|
80
|
+
# Examples:
|
|
81
|
+
# "memoizable -> active_support_cache_store -> active_record"
|
|
82
|
+
# "memoizable -> failover(primary: redis, secondary: memory)"
|
|
83
|
+
#
|
|
84
|
+
# Returns a String.
|
|
85
|
+
def adapter_stack
|
|
86
|
+
if respond_to?(:adapter) && adapter
|
|
87
|
+
"#{name} -> #{adapter.adapter_stack}"
|
|
88
|
+
else
|
|
89
|
+
name.to_s
|
|
90
|
+
end
|
|
91
|
+
end
|
|
76
92
|
end
|
|
77
93
|
end
|
|
78
94
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "flipper/adapters/wrapper"
|
|
2
|
+
|
|
1
3
|
module Flipper
|
|
2
4
|
module Adapters
|
|
3
5
|
class ActorLimit < Wrapper
|
|
@@ -5,13 +7,37 @@ module Flipper
|
|
|
5
7
|
|
|
6
8
|
attr_reader :limit
|
|
7
9
|
|
|
10
|
+
class << self
|
|
11
|
+
# Returns whether sync mode is enabled for the current thread.
|
|
12
|
+
# When sync mode is enabled, actor limits are not enforced,
|
|
13
|
+
# allowing sync operations to bring local state in line with
|
|
14
|
+
# remote state regardless of limits.
|
|
15
|
+
def sync_mode
|
|
16
|
+
Thread.current[:flipper_actor_limit_sync_mode]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def sync_mode=(value)
|
|
20
|
+
Thread.current[:flipper_actor_limit_sync_mode] = value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Executes a block with sync mode enabled. Actor limits will
|
|
24
|
+
# not be enforced within the block.
|
|
25
|
+
def with_sync_mode
|
|
26
|
+
old_value = sync_mode
|
|
27
|
+
self.sync_mode = true
|
|
28
|
+
yield
|
|
29
|
+
ensure
|
|
30
|
+
self.sync_mode = old_value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
8
34
|
def initialize(adapter, limit = 100)
|
|
9
35
|
super(adapter)
|
|
10
36
|
@limit = limit
|
|
11
37
|
end
|
|
12
38
|
|
|
13
39
|
def enable(feature, gate, resource)
|
|
14
|
-
if gate.is_a?(Flipper::Gates::Actor) && over_limit?(feature)
|
|
40
|
+
if gate.is_a?(Flipper::Gates::Actor) && !self.class.sync_mode && over_limit?(feature)
|
|
15
41
|
raise LimitExceeded, "Actor limit of #{@limit} exceeded for feature #{feature.key}. See https://www.flippercloud.io/docs/features/actors#limitations"
|
|
16
42
|
else
|
|
17
43
|
super
|