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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +8 -5
  3. data/.github/workflows/examples.yml +4 -3
  4. data/.github/workflows/release.yml +54 -0
  5. data/CLAUDE.md +16 -3
  6. data/README.md +4 -3
  7. data/examples/cloud/poll_interval/README.md +111 -0
  8. data/examples/cloud/poll_interval/client.rb +108 -0
  9. data/examples/cloud/poll_interval/server.rb +98 -0
  10. data/examples/expressions.rb +35 -11
  11. data/lib/flipper/adapter.rb +17 -1
  12. data/lib/flipper/adapters/actor_limit.rb +27 -1
  13. data/lib/flipper/adapters/cache_base.rb +21 -3
  14. data/lib/flipper/adapters/dual_write.rb +6 -2
  15. data/lib/flipper/adapters/failover.rb +9 -3
  16. data/lib/flipper/adapters/failsafe.rb +2 -2
  17. data/lib/flipper/adapters/http/client.rb +15 -4
  18. data/lib/flipper/adapters/http.rb +37 -2
  19. data/lib/flipper/adapters/instrumented.rb +2 -2
  20. data/lib/flipper/adapters/memoizable.rb +3 -3
  21. data/lib/flipper/adapters/memory.rb +1 -1
  22. data/lib/flipper/adapters/pstore.rb +1 -1
  23. data/lib/flipper/adapters/sync/feature_synchronizer.rb +5 -1
  24. data/lib/flipper/adapters/sync/synchronizer.rb +10 -5
  25. data/lib/flipper/adapters/sync.rb +7 -3
  26. data/lib/flipper/cli.rb +51 -0
  27. data/lib/flipper/cloud/configuration.rb +9 -4
  28. data/lib/flipper/cloud/dsl.rb +2 -2
  29. data/lib/flipper/cloud/middleware.rb +1 -1
  30. data/lib/flipper/cloud/migrate.rb +71 -0
  31. data/lib/flipper/cloud/telemetry.rb +1 -1
  32. data/lib/flipper/cloud.rb +1 -0
  33. data/lib/flipper/dsl.rb +1 -1
  34. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  35. data/lib/flipper/expressions/time.rb +8 -1
  36. data/lib/flipper/gates/expression.rb +2 -2
  37. data/lib/flipper/poller.rb +47 -8
  38. data/lib/flipper/version.rb +1 -1
  39. data/lib/flipper.rb +17 -1
  40. data/spec/flipper/adapter_spec.rb +20 -0
  41. data/spec/flipper/adapters/actor_limit_spec.rb +55 -0
  42. data/spec/flipper/adapters/dual_write_spec.rb +13 -0
  43. data/spec/flipper/adapters/failover_spec.rb +12 -0
  44. data/spec/flipper/adapters/http_spec.rb +151 -0
  45. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +12 -0
  46. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
  47. data/spec/flipper/adapters/sync_spec.rb +13 -0
  48. data/spec/flipper/cli_spec.rb +51 -0
  49. data/spec/flipper/cloud/configuration_spec.rb +6 -0
  50. data/spec/flipper/cloud/dsl_spec.rb +10 -2
  51. data/spec/flipper/cloud/middleware_spec.rb +34 -16
  52. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  53. data/spec/flipper/cloud/telemetry_spec.rb +1 -1
  54. data/spec/flipper/engine_spec.rb +2 -2
  55. data/spec/flipper/expressions/time_spec.rb +16 -0
  56. data/spec/flipper/gates/expression_spec.rb +82 -0
  57. data/spec/flipper/middleware/memoizer_spec.rb +37 -6
  58. data/spec/flipper/poller_spec.rb +347 -4
  59. data/spec/flipper_integration_spec.rb +133 -0
  60. data/spec/flipper_spec.rb +6 -1
  61. metadata +16 -112
  62. data/lib/flipper/expressions/duration.rb +0 -28
  63. data/spec/flipper/expressions/duration_spec.rb +0 -43
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f53e9dadf45ea1d00e7abd24358748347d516d761ea347c438057f62fdbc185
4
- data.tar.gz: 8c2a73d793d41ce64ed26c559bf532127f73e17e20944930aa6bf14aff355a57
3
+ metadata.gz: 881ca599099f433a4204f2a058c512b6b9198f529d1db4fd3a3445ba822dcd9b
4
+ data.tar.gz: 5ad5da793cec928e09357f2d46ddb139c159dfc1d5b9ddea4ae4c0c52193a75b
5
5
  SHA512:
6
- metadata.gz: af26df5e8c10540348e2b8dd31f11198dafdc8f3bcb6ef8e5361e3a764caa5a872734c64807746f1d077cfbc95df9143959e4c3446429dce1d5d871e46861fa7
7
- data.tar.gz: 2005a736ea665c359154bf85f56495fcf9b326e9ecb740d2e1aca0c52a698367b44334a90ae2b1277c2a3c0dcdddcc478eab3126975f210fc796a454202913f4
6
+ metadata.gz: 9b049bcd83af70810efd36f1873fb9d72cf22f95cf460d80829b7ad7ed68b7ef11e619b621ea001c230dfdbee43c453e76326de59be2306ee52b23444b50619b
7
+ data.tar.gz: fb82eca0e34233cf3fe9e001923f3b87ae2acf9f7dfdbbdce553d1b8408d5f23df075d789f5be2f975d6facf077637294f306d774e026005e4a7a13124866a3a
@@ -1,9 +1,11 @@
1
1
  name: CI
2
- on: [push, pull_request]
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.0
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@v4
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, pull_request]
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.0
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@v4
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
- ### Building and Releasing
21
- - `bundle exec rake build` - Build all gems into pkg/ directory
22
- - `bundle exec rake release` - Tag version, push to remote, and push gems (requires OTP)
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 to be whatever it should be and commit.
101
- 2. `script/release`
102
- 3. Create a new [GitHub Release](https://github.com/flippercloud/flipper/releases/new)
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
@@ -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
- scheduled_time_expression = Flipper.property(:now).gte(NOW)
210
- Flipper.enable :something, scheduled_time_expression
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
- refute Flipper.enabled?(:something, other_user)
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)
@@ -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