star_gates 0.0.0 → 0.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 +4 -4
- data/.dictate.toml +19 -0
- data/.pre-commit-config.yaml +20 -0
- data/CHANGELOG.md +60 -3
- data/LICENSE.txt +23 -16
- data/README.md +182 -16
- data/Rakefile +14 -4
- data/lib/star_gates/adapters/base_adapter.rb +65 -0
- data/lib/star_gates/adapters/memory_adapter.rb +93 -0
- data/lib/star_gates/adapters/redis_adapter.rb +119 -0
- data/lib/star_gates/application_star_gate.rb +398 -0
- data/lib/star_gates/circuit.rb +171 -0
- data/lib/star_gates/configuration.rb +172 -0
- data/lib/star_gates/dsl.rb +71 -2
- data/lib/star_gates/errors.rb +8 -0
- data/lib/star_gates/event_store/event_store.rb +79 -0
- data/lib/star_gates/event_store/memory_adapter.rb +78 -0
- data/lib/star_gates/events/base_event.rb +45 -0
- data/lib/star_gates/events/feature_events.rb +97 -0
- data/lib/star_gates/feature.rb +426 -0
- data/lib/star_gates/flags.rb +153 -0
- data/lib/star_gates/gate.rb +100 -0
- data/lib/star_gates/hexagonal/feature_aggregate.rb +250 -0
- data/lib/star_gates/hexagonal/feature_repository.rb +67 -0
- data/lib/star_gates/hexagonal/feature_service.rb +121 -0
- data/lib/star_gates/jsonrpc_rails_adapter.rb +251 -0
- data/lib/star_gates/projections/feature_projection.rb +224 -0
- data/lib/star_gates/registry.rb +58 -0
- data/lib/star_gates/telemetry/opentelemetry_adapter.rb +252 -0
- data/lib/star_gates/telemetry/prometheus_adapter.rb +183 -0
- data/lib/star_gates/telemetry/telemetry_adapter.rb +160 -0
- data/lib/star_gates/version.rb +1 -1
- data/lib/star_gates.rb +308 -8
- metadata +95 -14
- data/sig/star_gates.rbs +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9150966ca03ffe47ce938c669d38ab3291e25b6b7aaea9560e89099ec255a65d
|
|
4
|
+
data.tar.gz: 0454b43be544e3c50026811f5d44ec3a28903c4ff824fcf3b855aaf70a733aba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d7802feb0b5ebc3aefcd585224cfd5dda9f95e9e8f23ee5653a087f5326d14764ce7e6a06d16ca01cafd60570325c910cb776464b2abce14b2ef05b0a202407
|
|
7
|
+
data.tar.gz: 3e57ca2506afe07616e9c884f1f74228c2f380601a0ca4779b311a422aa0c215d228835e9160b0689db744ec788252b752f5133703992063a06c6c7db87254d7
|
data/.dictate.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[decree.supreme]
|
|
2
|
+
trailing_whitespace = "deny"
|
|
3
|
+
tabs_vs_spaces = "spaces"
|
|
4
|
+
tab_width = 2
|
|
5
|
+
final_newline = "require"
|
|
6
|
+
line_endings = "lf"
|
|
7
|
+
blank_line_whitespace = "deny"
|
|
8
|
+
|
|
9
|
+
[decree.supreme.ignore.tab-character]
|
|
10
|
+
filenames = ["Makefile", "GNUmakefile", "makefile"]
|
|
11
|
+
extensions = ["md", "mdx", "tsv"]
|
|
12
|
+
|
|
13
|
+
[decree.ruby]
|
|
14
|
+
max_line_length = 120
|
|
15
|
+
max_lines = 400
|
|
16
|
+
ignore_comments = true
|
|
17
|
+
ignore_blank_lines = true
|
|
18
|
+
method_visibility_order = ["public", "protected", "private"]
|
|
19
|
+
comment_spacing = true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v5.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-added-large-files
|
|
9
|
+
- id: check-merge-conflict
|
|
10
|
+
- id: check-json
|
|
11
|
+
- id: check-case-conflict
|
|
12
|
+
|
|
13
|
+
- repo: local
|
|
14
|
+
hooks:
|
|
15
|
+
- id: dictator
|
|
16
|
+
name: dictator
|
|
17
|
+
entry: dictator lint
|
|
18
|
+
language: system
|
|
19
|
+
types: [text]
|
|
20
|
+
require_serial: true
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Unreleased
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 0.2.0 (2026-10-28)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Use explicit rollout `key:` and `groups:` context. Group predicates receive the
|
|
10
|
+
full context; simulation, debugging, and scenario analysis use key-based inputs
|
|
11
|
+
and results. Remove user-specific helpers and telemetry labels.
|
|
12
|
+
- Require Ruby 4.0 or newer.
|
|
13
|
+
- Require StateMachines 0.202.0 or newer.
|
|
14
|
+
- Change the license to BSD-3-Clause.
|
|
15
|
+
- Use ActiveSupport duration helpers without overriding Rails behavior.
|
|
16
|
+
- Preserve fractional seconds in circuit cooldowns and failure windows.
|
|
17
|
+
- Consolidate documentation and remove generated repository artifacts, keeping the space-fleet examples.
|
|
18
|
+
- Move demos into the Rails dummy app and rewrite ship systems as StarGates gates.
|
|
19
|
+
- Run gem and Rails integration tests separately, with explicit test discovery and Rails setup.
|
|
20
|
+
- Use the Dictator GitHub Action and a project decree for structural linting.
|
|
21
|
+
- Run both test suites with padded_cell and Minitest 6.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Keep percentage rollout cohorts stable across workers and restarts with CRC32
|
|
26
|
+
key buckets. Upgrading reassigns cohorts previously based on Ruby's
|
|
27
|
+
process-randomized string hashes.
|
|
28
|
+
- Correct dummy-app feature routing, persistent feature transitions, and circuit statistics.
|
|
29
|
+
- Make weather demo failures explicit instead of random.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Boolean flags with environment, configuration, and runtime overrides.
|
|
34
|
+
- Flag status, metadata, and debugging helpers.
|
|
35
|
+
|
|
36
|
+
## 0.1.0 (2025-10-30)
|
|
37
|
+
|
|
38
|
+
### Security
|
|
39
|
+
|
|
40
|
+
- Use safe YAML loading for configuration files.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- Correct percentage-string parsing for feature rollouts.
|
|
45
|
+
- Remove dead time-parsing code.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- ChronoMachines retry policies with exponential backoff and jitter.
|
|
50
|
+
- Circuit retry instrumentation.
|
|
51
|
+
- Feature state transitions, rollout targeting, and automatic circuit-triggered rollback.
|
|
52
|
+
- Transition history and introspection.
|
|
53
|
+
- Regression coverage for configuration security, rollouts, concurrency, and retries.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- Use immutable `Data.define` mock users instead of `OpenStruct`.
|
|
58
|
+
- Use concurrent-ruby primitives for thread-safe registries and transition history.
|
|
59
|
+
|
|
60
|
+
## 0.0.0
|
|
61
|
+
|
|
62
|
+
- Internal development version.
|
data/LICENSE.txt
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
BSD 3-Clause License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2025 Abdelkader Boudih
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
16
|
+
may be used to endorse or promote products derived from this software
|
|
17
|
+
without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
CHANGED
|
@@ -1,39 +1,205 @@
|
|
|
1
1
|
# StarGates
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/star_gates)
|
|
4
|
+
[](https://github.com/seuros/star_gates/actions/workflows/ci.yml)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Feature flags, gradual rollouts, and circuit-protected execution for Ruby.
|
|
7
|
+
StarGates uses StateMachines for feature transitions, BreakerMachines for circuit
|
|
8
|
+
protection, and ChronoMachines for retries.
|
|
6
9
|
|
|
7
10
|
## Installation
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
Requires **Ruby 4.0+** and **ActiveSupport 8.1.1+**. Rails is optional.
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
```ruby
|
|
15
|
+
gem 'star_gates'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bundle install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Boolean flags
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require 'star_gates'
|
|
26
|
+
|
|
27
|
+
StarGates.flags.configuration = { copilot: true }
|
|
28
|
+
StarGates.flags.enabled?(:copilot)
|
|
29
|
+
StarGates.flags.enable(:preview, metadata: { reason: 'testing' })
|
|
30
|
+
StarGates.flags.disable(:preview)
|
|
31
|
+
StarGates.flags.debug(:preview)
|
|
32
|
+
StarGates.flags.reset!
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Precedence: nonblank `FEATURE_<NAME>` environment variable, boolean configuration,
|
|
36
|
+
runtime override, then `false`. Names are case-insensitive; dashes become underscores.
|
|
37
|
+
Truthy strings are `true`, `1`, `yes`, and `on` (case-insensitive).
|
|
38
|
+
|
|
39
|
+
For Rails, keep defaults explicit and resolve them lazily:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# config/initializers/feature_flags.rb
|
|
43
|
+
Rails.application.config.x.feature_flags = { copilot: true }
|
|
44
|
+
StarGates.flags.configuration = -> { Rails.application.config.x.feature_flags }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`StarGates::Flags.new(env: {}, configuration: {})` creates an isolated registry.
|
|
48
|
+
`all`, `enabled_flags`, `summary`, and `flag_status` expose values and their sources.
|
|
49
|
+
Runtime transitions reuse `StarGates::Feature`; changes and metadata are process-local,
|
|
50
|
+
not persisted or synchronized across workers. `reset!` clears both runtime state and
|
|
51
|
+
metadata, not environment/configuration. `StarGates.reset!` resets gates separately.
|
|
52
|
+
|
|
53
|
+
## Gates and rollouts
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
StarGates.define do
|
|
57
|
+
gate :payments do
|
|
58
|
+
feature rollout: 25.percent
|
|
59
|
+
circuit threshold: 3, within: 1.minute, reset_after: 30.seconds
|
|
60
|
+
|
|
61
|
+
run { |context| PaymentAPI.charge(context[:amount]) }
|
|
62
|
+
fallback { |context| LegacyPaymentAPI.charge(context[:amount]) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
gate = StarGates[:payments]
|
|
67
|
+
gate.feature.rollout_canary
|
|
68
|
+
gate.call(key: 'account:42', amount: 100)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Features start disabled. Use `rollout_canary` for percentage/group targeting,
|
|
72
|
+
`enable` for full rollout, and `disable` to turn a feature off. Pass a stable
|
|
73
|
+
string or integer `key:` for percentage targeting, such as an account, resident,
|
|
74
|
+
or device identifier.
|
|
75
|
+
|
|
76
|
+
Percentage targeting stays consistent across workers and restarts. Increasing
|
|
77
|
+
the rollout percentage retains keys already included.
|
|
78
|
+
|
|
79
|
+
For group targeting, pass `groups: [:beta]` to match `feature group: :beta`.
|
|
80
|
+
A predicate such as `feature group: ->(context) { context[:region] == 'eu' }`
|
|
81
|
+
receives the full context passed to the gate.
|
|
82
|
+
|
|
83
|
+
When a circuit opens, its feature rolls back from enabled to canary, or from canary
|
|
84
|
+
to disabled. Gates use their fallback when the feature is unavailable or execution
|
|
85
|
+
fails. Rails duration helpers retain their normal behavior, including fractional
|
|
86
|
+
seconds such as `0.5.seconds`.
|
|
87
|
+
|
|
88
|
+
### Retries
|
|
89
|
+
|
|
90
|
+
Pass `retry_config` to a gate's `circuit` declaration:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
circuit threshold: 5,
|
|
94
|
+
within: 1.minute,
|
|
95
|
+
retry_config: {
|
|
96
|
+
max_attempts: 3,
|
|
97
|
+
base_delay: 0.1,
|
|
98
|
+
multiplier: 2,
|
|
99
|
+
max_delay: 5
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Retries are opt-in and use ChronoMachines.
|
|
104
|
+
|
|
105
|
+
## Rails
|
|
106
|
+
|
|
107
|
+
Define gates in an initializer and call them from controllers or services:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class PaymentsController < ApplicationController
|
|
111
|
+
def create
|
|
112
|
+
result = StarGates[:payments].call(key: "account:#{current_account.id}", amount: params[:amount])
|
|
113
|
+
render json: result
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The Rails integration fixture lives in `test/dummy/` and uses the root Gemfile.
|
|
119
|
+
|
|
120
|
+
## Instrumentation
|
|
121
|
+
|
|
122
|
+
StarGates emits events through `ActiveSupport::Notifications`:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
ActiveSupport::Notifications.subscribe('star_gates.circuit.auto_rollback') do |*args|
|
|
126
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
127
|
+
Rails.logger.warn(event.payload)
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Circuit events include `circuit.auto_rollback`, `circuit.half_open`,
|
|
132
|
+
`circuit.recovered`, and `circuit.retry`, prefixed with `star_gates.`.
|
|
12
133
|
|
|
13
|
-
|
|
14
|
-
|
|
134
|
+
## Examples
|
|
135
|
+
|
|
136
|
+
### Space fleet
|
|
137
|
+
|
|
138
|
+
All demos live in `test/dummy/`. The fleet uses real `StarGates::Gate` instances
|
|
139
|
+
for feature rollout, circuit protection, automatic rollback, and fallbacks.
|
|
140
|
+
BreakerMachines is the underlying circuit engine, not a separate ship API.
|
|
141
|
+
|
|
142
|
+
- [Spaceship](test/dummy/app/models/spaceship.rb): warp drive, fusion reactor, and navigation circuits.
|
|
143
|
+
- [Spaceflight systems](test/dummy/app/models/spaceflight_systems.rb): simulated ship subsystems.
|
|
144
|
+
- [Ship hierarchy](test/dummy/app/models/base_ship.rb): shared systems for the fleet.
|
|
145
|
+
- [Corellian freighter](test/dummy/app/models/corellian_freighter.rb): specialized ship behavior.
|
|
146
|
+
- [RMNS Atlas Monkey](test/dummy/app/models/rmns_atlas_monkey.rb): the Universal Commentary Engine.
|
|
147
|
+
|
|
148
|
+
Run the fleet demo without starting Rails:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
bundle exec ruby test/dummy/demos/space_fleet.rb
|
|
15
152
|
```
|
|
16
153
|
|
|
17
|
-
|
|
154
|
+
It demonstrates disabled-system fallback, pilot-only canary access, full rollout,
|
|
155
|
+
hardware failures opening a circuit, automatic feature rollback, and explicit
|
|
156
|
+
operator-approved recovery. Each ship's gates have independent state.
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
require_relative 'test/dummy/app/models/spaceship'
|
|
18
160
|
|
|
19
|
-
|
|
20
|
-
|
|
161
|
+
ship = Spaceship.new('SG Atlas', 'SG-1')
|
|
162
|
+
warp = ship.gate(:warp_drive) # A StarGates::Gate, with a feature and a circuit.
|
|
163
|
+
warp.feature.rollout_canary
|
|
164
|
+
ship.engage_warp('Alpha Centauri', key: 'crew:1', groups: [:pilots])
|
|
21
165
|
```
|
|
22
166
|
|
|
23
|
-
|
|
167
|
+
### Application examples
|
|
24
168
|
|
|
25
|
-
|
|
169
|
+
- [Payment feature](test/dummy/demos/payment_feature.rb): state-specific feature behavior.
|
|
170
|
+
- [Transition history](test/dummy/demos/transition_demo.rb): recorded feature transitions.
|
|
171
|
+
- [Introspection](test/dummy/demos/introspection_demo.rb): status, analytics, and context simulation.
|
|
172
|
+
- [Application payment feature](test/dummy/demos/application_payment_feature.rb): application-level features.
|
|
173
|
+
- [JSON-RPC controller](test/dummy/demos/jsonrpc_rails_controller.rb): optional JSON-RPC integration.
|
|
174
|
+
|
|
175
|
+
Run standalone examples from the repository root:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
bundle exec ruby test/dummy/demos/payment_feature.rb
|
|
179
|
+
```
|
|
26
180
|
|
|
27
181
|
## Development
|
|
28
182
|
|
|
29
|
-
|
|
183
|
+
```sh
|
|
184
|
+
bin/setup
|
|
185
|
+
bundle exec rake test
|
|
186
|
+
bundle exec rake test:rails
|
|
187
|
+
dictator lint .
|
|
188
|
+
```
|
|
30
189
|
|
|
31
|
-
|
|
190
|
+
Tests run with padded_cell and Minitest 6. Gem tests and Rails integration tests
|
|
191
|
+
run in separate processes. `bundle exec rake` runs both suites. To select gem tests
|
|
192
|
+
directly or pass runner options:
|
|
32
193
|
|
|
33
|
-
|
|
194
|
+
```sh
|
|
195
|
+
bundle exec padded_cell test/gate_test.rb
|
|
196
|
+
bundle exec rake test TESTOPTS='-j 4 --seed 1234'
|
|
197
|
+
```
|
|
34
198
|
|
|
35
|
-
|
|
199
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
36
200
|
|
|
37
201
|
## License
|
|
38
202
|
|
|
39
|
-
|
|
203
|
+
[BSD-3-Clause](LICENSE.txt).
|
|
204
|
+
|
|
205
|
+
> In space, nobody can hear your feature flags fail, but they'll notice when your fleet survives.
|
data/Rakefile
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'shellwords'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
desc 'Run gem tests with padded_cell'
|
|
7
|
+
task :test do
|
|
8
|
+
sh 'padded_cell', 'test/*_test.rb', *Shellwords.split(ENV.fetch('TESTOPTS', ''))
|
|
9
|
+
end
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
namespace :test do
|
|
12
|
+
desc 'Run Rails integration tests with padded_cell'
|
|
13
|
+
task :rails do
|
|
14
|
+
sh 'padded_cell', 'test/dummy/test', *Shellwords.split(ENV.fetch('TESTOPTS', ''))
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
task default: [:test, 'test:rails']
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StarGates
|
|
4
|
+
module Adapters
|
|
5
|
+
class BaseAdapter
|
|
6
|
+
def initialize(config = {})
|
|
7
|
+
@config = config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Persistence operations
|
|
11
|
+
def store(key, value)
|
|
12
|
+
raise NotImplementedError, 'Subclasses must implement #store'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch(key)
|
|
16
|
+
raise NotImplementedError, 'Subclasses must implement #fetch'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def delete(key)
|
|
20
|
+
raise NotImplementedError, 'Subclasses must implement #delete'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def exists?(key)
|
|
24
|
+
raise NotImplementedError, 'Subclasses must implement #exists?'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Caching operations
|
|
28
|
+
def cache_fetch(key, expires_in: nil, &block)
|
|
29
|
+
if exists?(key)
|
|
30
|
+
fetch(key)
|
|
31
|
+
elsif block_given?
|
|
32
|
+
value = block.call
|
|
33
|
+
store(key, value) if value
|
|
34
|
+
value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def cache_write(key, value, expires_in: nil)
|
|
39
|
+
store(key, value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cache_delete(key)
|
|
43
|
+
delete(key)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Messaging operations
|
|
47
|
+
def publish(topic, message)
|
|
48
|
+
# Default no-op implementation
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def subscribe(topic, &)
|
|
52
|
+
# Default no-op implementation
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Health check
|
|
56
|
+
def healthy?
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
protected
|
|
61
|
+
|
|
62
|
+
attr_reader :config
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_adapter'
|
|
4
|
+
require 'concurrent'
|
|
5
|
+
|
|
6
|
+
module StarGates
|
|
7
|
+
module Adapters
|
|
8
|
+
class MemoryAdapter < BaseAdapter
|
|
9
|
+
def initialize(config = {})
|
|
10
|
+
super
|
|
11
|
+
@storage = Concurrent::Hash.new
|
|
12
|
+
@subscribers = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def store(key, value)
|
|
17
|
+
@storage[key] = value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fetch(key)
|
|
21
|
+
@storage[key]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def delete(key)
|
|
25
|
+
@storage.delete(key)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def exists?(key)
|
|
29
|
+
@storage.key?(key)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cache_fetch(key, expires_in: nil, &block)
|
|
33
|
+
# Memory adapter doesn't support expiration, but we can implement basic caching
|
|
34
|
+
if @storage.key?(key)
|
|
35
|
+
@storage[key]
|
|
36
|
+
elsif block_given?
|
|
37
|
+
value = block.call
|
|
38
|
+
@storage[key] = value if value
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def cache_write(key, value, expires_in: nil)
|
|
44
|
+
@storage[key] = value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cache_delete(key)
|
|
48
|
+
@storage.delete(key)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def publish(topic, message)
|
|
52
|
+
subscribers = @subscribers[topic].to_a
|
|
53
|
+
return if subscribers.empty?
|
|
54
|
+
|
|
55
|
+
# Publish to subscribers in background fibers if available
|
|
56
|
+
if defined?(Fiber.scheduler) && Fiber.scheduler
|
|
57
|
+
subscribers.each do |subscriber|
|
|
58
|
+
Fiber.schedule { subscriber.call(message) }
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
subscribers.each { |subscriber| subscriber.call(message) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def subscribe(topic, &block)
|
|
66
|
+
@subscribers[topic] << block
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def clear!
|
|
70
|
+
@mutex.synchronize do
|
|
71
|
+
@storage.clear
|
|
72
|
+
@subscribers.clear
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def size
|
|
77
|
+
@storage.size
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def topics_count
|
|
81
|
+
@subscribers.size
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def subscribers_count(topic)
|
|
85
|
+
@subscribers[topic].size
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def healthy?
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|