singulus 0.1.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 +7 -0
- data/CHANGELOG.md +77 -0
- data/LICENSE.txt +21 -0
- data/README.md +248 -0
- data/lib/singulus/internal/configuration.rb +34 -0
- data/lib/singulus/internal/constructor_guard.rb +27 -0
- data/lib/singulus/internal/errors.rb +15 -0
- data/lib/singulus/internal/instance_guard.rb +37 -0
- data/lib/singulus/internal/mutation_guard.rb +33 -0
- data/lib/singulus/internal/reflection_guard.rb +47 -0
- data/lib/singulus/internal/runtime_hardening.rb +135 -0
- data/lib/singulus/internal/singleton_class_methods.rb +64 -0
- data/lib/singulus/multiton.rb +316 -0
- data/lib/singulus/version.rb +5 -0
- data/lib/singulus.rb +182 -0
- metadata +60 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ac98626fb3bc3db2c925dc5a31d25655501aa36544907ee9ba95d87b3e7e3917
|
|
4
|
+
data.tar.gz: 371565dd472005e3a4038a0fd7ad1d9fa2679fe237b610072101c5a541b7f821
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5a6826300e20a88e8ae1e811cd1525c7f3cd38dcb6628d22608c92e2fc9ebc7029774288b2b990742783222f98f9054d188e5d892852a3bc7da8cf74181f71cc
|
|
7
|
+
data.tar.gz: 8d7b79fbcaac93d423b22c82856539e57c6ed3f718cbed8a22a1ca2f6760c45d2e47c42c1db3a11bfb1bda1033cf6800cfcf3029474693add6f54e8fb876855f
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Completed the final RuboCop cleanup across Multiton and specs.
|
|
12
|
+
- Removed `refuse_coverage_drop`; coverage is enforced by absolute minimums of 95% line and 90% branch.
|
|
13
|
+
|
|
14
|
+
- Normalized RuboCop policy so local Rubcraft checks and GitHub CI enforce the same repository-local configuration.
|
|
15
|
+
- Fixed production-code indentation/alignment issues introduced during the `Singulus::Internal` refactor.
|
|
16
|
+
- Removed the redundant `thread` require.
|
|
17
|
+
- Added RubyGems MFA metadata.
|
|
18
|
+
- Moved development tooling declarations to Gemfile.
|
|
19
|
+
- Kept intentional reflection interception, internal ivar naming, Multiton complexity, and contract-style specs explicitly documented in RuboCop configuration.
|
|
20
|
+
|
|
21
|
+
- Added a small coverage buffer for safe mutation/reflection fallthrough paths and repeated runtime-hardening activation.
|
|
22
|
+
|
|
23
|
+
- Made RuboCop configuration explicit and repository-local for identical local/CI behavior.
|
|
24
|
+
- CI and release workflows now pass `.rubocop.yml` explicitly.
|
|
25
|
+
- Standardized the project on double-quoted Ruby strings.
|
|
26
|
+
|
|
27
|
+
- Added `bundler-audit` as a development dependency so Rubcraft's mandatory audit gate runs through the repository bundle.
|
|
28
|
+
|
|
29
|
+
- Fixed `InstanceGuard#clone` so it does not forward an implicit `freeze:` keyword to Ruby Singleton implementations that accept no arguments.
|
|
30
|
+
- Corrected standard-mode reflection coverage: reflective constructor access is intentionally available when local hardening is disabled.
|
|
31
|
+
- Corrected runtime-hardening coverage to distinguish blocked reflection gateways from safe non-gateway UnboundMethod binding.
|
|
32
|
+
|
|
33
|
+
- Added branch-focused behavior coverage for standard-mode fallthroughs, reflection guards, runtime-hardening negative paths, and valid Multiton retention branches.
|
|
34
|
+
|
|
35
|
+
- Corrected runtime Method composition specs to distinguish `Method#<<`/`Method#>>` from `Proc#<<` composition semantics.
|
|
36
|
+
|
|
37
|
+
- Expanded behavioral coverage for configuration normalization, public reflection guards, Multiton registry edge cases, forwarding semantics, standard-mode inheritance, and runtime Method composition.
|
|
38
|
+
|
|
39
|
+
- Corrected RSpec expectations for sealed constructor access.
|
|
40
|
+
- Corrected Multiton key-stabilization coverage to use genuinely mutable input under `frozen_string_literal`.
|
|
41
|
+
- Corrected Hash identifiers to be passed positionally under Ruby 3 keyword argument semantics.
|
|
42
|
+
|
|
43
|
+
## [0.1.0] - 2026-08-21
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- Reduced the public root API to `Singulus::Singleton`, `Singulus::Multiton`, `Singulus::Error`, `Singulus::VERSION`, and the configuration facade.
|
|
48
|
+
- Moved implementation classes, guards, configuration objects, and specialized exceptions behind a private `Singulus::Internal` namespace.
|
|
49
|
+
- Marked Multiton implementation constants (`RETENTIONS`, `Entry`, and `ClassMethods`) private.
|
|
50
|
+
- Added a public API contract spec to prevent accidental constant leakage.
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- `Singulus::Singleton` for one managed instance per class.
|
|
55
|
+
- `Singulus::Multiton` for one managed instance per normalized identifier.
|
|
56
|
+
- `:standard`, `:strict`, and `:runtime` hardening modes.
|
|
57
|
+
- Include-time configuration with `Singulus::Singleton.with(...)` and `Singulus::Multiton.with(...)`.
|
|
58
|
+
- Two-step `singulus` DSL in addition to include-time `.with(...)` configuration.
|
|
59
|
+
- Multiton key normalization and defensive stabilization of common mutable keys.
|
|
60
|
+
- `:forever`, `:lru`, `:ttl`, `:bounded`, and `:weak` Multiton retention strategies.
|
|
61
|
+
- Explicit Multiton lifecycle operations: `instance?`, `instance_count`, `instance_keys`, `delete_instance`, and `clear_instances`.
|
|
62
|
+
- Thread-safe Singleton and Multiton initialization.
|
|
63
|
+
- Recursive Multiton initialization detection.
|
|
64
|
+
- Constructor, duplication, inheritance, mutation, and reflection guards in hardened modes.
|
|
65
|
+
- Runtime protection for common captured `Method` and `UnboundMethod` constructor/reflection paths.
|
|
66
|
+
- RSpec, RuboCop, SimpleCov, GitHub Actions CI, and RubyGems Trusted Publishing workflow.
|
|
67
|
+
|
|
68
|
+
### Security
|
|
69
|
+
|
|
70
|
+
- Guard `new` and `allocate` from the moment hardened Singulus modules are included.
|
|
71
|
+
- Prevent constructor bypass by changing constructor visibility.
|
|
72
|
+
- Reject constructor redefinition attempts in hardened modes.
|
|
73
|
+
- Protect common reflective constructor access paths.
|
|
74
|
+
- Protect captured `Method` and `UnboundMethod` paths in `:runtime` mode.
|
|
75
|
+
|
|
76
|
+
[Unreleased]: https://github.com/Rubcraft/singulus/compare/v0.1.0...HEAD
|
|
77
|
+
[0.1.0]: https://github.com/Rubcraft/singulus/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
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:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Singulus
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Rubcraft/singulus/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/singulus)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
`Singulus` adds configurable Ruby-level hardening around two instance-uniqueness patterns:
|
|
8
|
+
|
|
9
|
+
- `Singulus::Singleton` — one instance per class and Ruby process.
|
|
10
|
+
- `Singulus::Multiton` — one registered instance per class and normalized key.
|
|
11
|
+
|
|
12
|
+
It builds on Ruby's standard `Singleton` for the classic Singleton implementation and provides its own thread-safe keyed registry for Multiton.
|
|
13
|
+
|
|
14
|
+
Singulus exposes one supported entry point:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require "singulus"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The supported root API is intentionally small:
|
|
21
|
+
|
|
22
|
+
- `Singulus::Singleton`
|
|
23
|
+
- `Singulus::Multiton`
|
|
24
|
+
- `Singulus::Error` as the public base exception
|
|
25
|
+
- `Singulus.configure`, `Singulus.configuration`, and `Singulus.reset_configuration!`
|
|
26
|
+
- `Singulus::VERSION`
|
|
27
|
+
|
|
28
|
+
All implementation classes, guards, configuration types, and specialized errors live behind a private `Singulus::Internal` namespace and are not public constants. No pre-release compatibility entry points are shipped.
|
|
29
|
+
|
|
30
|
+
## Security boundary
|
|
31
|
+
|
|
32
|
+
This gem is aggressive Ruby-level hardening, **not a sandbox**. Code executing in the same Ruby process can reopen core classes, use native extensions, replace guards, or otherwise modify the runtime. `:runtime` closes many common `Method`/`UnboundMethod` bypasses, but it cannot create a cryptographic or process-isolation boundary.
|
|
33
|
+
|
|
34
|
+
## Modes
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
Singulus.configure do |config|
|
|
38
|
+
config.default_mode = :strict
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Available modes:
|
|
43
|
+
|
|
44
|
+
- `:standard` — minimal policy. Classic Singleton delegates to Ruby `Singleton`; Multiton keeps constructors private but does not install local hardening policy.
|
|
45
|
+
- `:strict` — constructor reflection guards, mutation protection, duplication protection, inheritance restrictions, and constructor sealing for classic Singleton.
|
|
46
|
+
- `:runtime` — everything in `:strict`, plus process-wide `Method` and `UnboundMethod` guards that activate only for classes marked as runtime-hardened.
|
|
47
|
+
|
|
48
|
+
The default is `:strict`.
|
|
49
|
+
|
|
50
|
+
### Configure directly from `include`
|
|
51
|
+
|
|
52
|
+
`Singleton` and `Multiton` also expose `.with(...)`, which creates an independent configured inclusion module. It does not mutate the shared `Singulus::Singleton` or `Singulus::Multiton` module.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class Configuration
|
|
56
|
+
include Singulus::Singleton.with(:strict)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class RuntimeConfiguration
|
|
60
|
+
include Singulus::Singleton.with(mode: :runtime)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class TenantConfiguration
|
|
64
|
+
include Singulus::Multiton.with(
|
|
65
|
+
:runtime,
|
|
66
|
+
retention: :lru,
|
|
67
|
+
max_size: 5_000
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The existing two-step DSL remains supported and equivalent:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
include Singulus::Singleton
|
|
76
|
+
singulus mode: :strict
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Classic Singleton
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
require "singulus"
|
|
83
|
+
|
|
84
|
+
class Configuration
|
|
85
|
+
include Singulus::Singleton.with(:runtime)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Configuration.instance.equal?(Configuration.instance)
|
|
89
|
+
# => true
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
In hardened modes, direct constructor access is guarded from the moment the module is included, including attempts to change constructor visibility. Common reflection routes, duplication, constructor redefinition, and inheritance are rejected. After the first `instance`, classic Singleton also seals its constructor lookup path.
|
|
93
|
+
|
|
94
|
+
## Multiton: one instance per identifier
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class TenantConfiguration
|
|
98
|
+
include Singulus::Multiton.with(:strict)
|
|
99
|
+
|
|
100
|
+
def initialize(tenant_id)
|
|
101
|
+
@tenant_id = tenant_id
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
one = TenantConfiguration.instance_for(10)
|
|
106
|
+
two = TenantConfiguration.instance_for(10)
|
|
107
|
+
other = TenantConfiguration.instance_for(20)
|
|
108
|
+
|
|
109
|
+
one.equal?(two) # => true
|
|
110
|
+
one.equal?(other) # => false
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`instance_for(identifier, *args, **kwargs)` passes the original identifier as the first initializer argument. Additional initializer arguments are used only when an instance for that normalized key does not already exist.
|
|
114
|
+
|
|
115
|
+
### Stable tenant keys
|
|
116
|
+
|
|
117
|
+
Prefer immutable identifiers such as integers, symbols, UUID strings, or normalized composite keys. Strings, arrays, and hashes returned as keys are defensively duplicated/frozen recursively where practical.
|
|
118
|
+
|
|
119
|
+
For domain objects, define a key normalizer:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
class TenantConfiguration
|
|
123
|
+
include Singulus::Multiton
|
|
124
|
+
|
|
125
|
+
multiton_key { |tenant| tenant.id }
|
|
126
|
+
|
|
127
|
+
def initialize(tenant)
|
|
128
|
+
@tenant = tenant
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This avoids retaining an ActiveRecord object itself as the registry key.
|
|
134
|
+
|
|
135
|
+
## Multiton lifecycle
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
TenantConfiguration.instance?(10)
|
|
139
|
+
TenantConfiguration.instance_count
|
|
140
|
+
TenantConfiguration.instance_keys
|
|
141
|
+
TenantConfiguration.delete_instance(10)
|
|
142
|
+
TenantConfiguration.clear_instances
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`delete_instance` and `clear_instances` explicitly release registry ownership. If another part of the application still holds the old object, a later `instance_for` can create a new object for the same key. Therefore uniqueness is **registry/lifecycle scoped**, not a claim that no second live Ruby object can ever exist after explicit release or eviction.
|
|
146
|
+
|
|
147
|
+
## Retention strategies
|
|
148
|
+
|
|
149
|
+
The default is strict registry retention:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
class TenantConfiguration
|
|
153
|
+
include Singulus::Multiton
|
|
154
|
+
multiton_retention :forever
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Supported strategies:
|
|
159
|
+
|
|
160
|
+
### `:forever`
|
|
161
|
+
|
|
162
|
+
Keeps each registered instance until `delete_instance`/`clear_instances` or process exit. This is the strongest per-key identity guarantee supplied by Multiton.
|
|
163
|
+
|
|
164
|
+
### `:lru`
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
multiton_retention :lru, max_size: 5_000
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Bounds memory by evicting the least recently used registry entry. Once evicted, the same key can create a new instance later.
|
|
171
|
+
|
|
172
|
+
### `:ttl`
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
multiton_retention :ttl, ttl: 300
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Keeps an instance for a fixed TTL measured with `Process::CLOCK_MONOTONIC`. Expiration is based on creation time, not sliding access time. After expiration, the same key can create a new instance.
|
|
179
|
+
|
|
180
|
+
### `:bounded`
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
multiton_retention :bounded, ttl: 300, max_size: 5_000
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Combines TTL expiration with an LRU capacity bound. Entries are evicted when they expire or when the registry exceeds `max_size`. Use this instead of passing `max_size` to `:ttl` or `ttl` to `:lru`; those ambiguous configurations are rejected.
|
|
187
|
+
|
|
188
|
+
### `:weak`
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
multiton_retention :weak
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Stores weak references. Singulus reuses the same object while it remains alive elsewhere, but the garbage collector may reclaim it when no strong references remain. A later `instance_for` can therefore create a new instance. `:weak` accepts neither `ttl` nor `max_size`.
|
|
195
|
+
|
|
196
|
+
| Strategy | `ttl` | `max_size` | Identity scope |
|
|
197
|
+
| --- | --- | --- | --- |
|
|
198
|
+
| `:forever` | no | no | until explicit release/process exit |
|
|
199
|
+
| `:lru` | no | required | while retained by capacity |
|
|
200
|
+
| `:ttl` | required | no | until expiration |
|
|
201
|
+
| `:bounded` | required | required | until expiration or capacity eviction |
|
|
202
|
+
| `:weak` | no | no | while the object remains strongly referenced |
|
|
203
|
+
|
|
204
|
+
`multiton_key` and retention configuration are locked while the registry contains instances. Call `clear_instances` before changing them deliberately.
|
|
205
|
+
|
|
206
|
+
## Runtime hardening
|
|
207
|
+
|
|
208
|
+
Runtime mode additionally guards common bypasses involving:
|
|
209
|
+
|
|
210
|
+
- `Method#call`
|
|
211
|
+
- `Method#[]`
|
|
212
|
+
- `Method#to_proc`
|
|
213
|
+
- `Method#>>` / `Method#<<`
|
|
214
|
+
- `UnboundMethod#bind`
|
|
215
|
+
- `UnboundMethod#bind_call`
|
|
216
|
+
- captured reflection gateways such as `Object#method` and `BasicObject#__send__`
|
|
217
|
+
|
|
218
|
+
The patches are installed process-wide once required, but their rejection logic only activates for classes marked `:runtime`.
|
|
219
|
+
|
|
220
|
+
Enable runtime mode as early as possible during boot:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
Singulus.configure do |config|
|
|
224
|
+
config.default_mode = :runtime
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
For Rails, place this in an early initializer before code that might capture constructor references.
|
|
229
|
+
|
|
230
|
+
A `Proc` created from a constructor `Method` *before* Singulus runtime hardening is enabled cannot be retroactively identified as constructor-derived. Enable `:runtime` during boot, before untrusted or extension code can capture such references.
|
|
231
|
+
|
|
232
|
+
## Development
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
bundle install
|
|
236
|
+
bundle exec rubocop --parallel
|
|
237
|
+
bundle exec rspec
|
|
238
|
+
bundle exec bundler-audit check --update
|
|
239
|
+
bundle exec rake build
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
SimpleCov enforces line and branch coverage, while CI tests supported Ruby versions independently from RuboCop.
|
|
243
|
+
|
|
244
|
+
## Releasing
|
|
245
|
+
|
|
246
|
+
Repository initialization, version-control operations, and release-tag creation are managed by the Rubcraft Toolkit. Singulus itself does not prescribe or duplicate those commands.
|
|
247
|
+
|
|
248
|
+
The repository keeps only the CI/publish boundary: `.github/workflows/release.yml` reacts to a version tag produced by the Toolkit, verifies that it matches `Singulus::VERSION`, runs quality checks, builds the gem, and publishes through RubyGems Trusted Publishing (OIDC). Configure a GitHub environment named `release` and the matching RubyGems Trusted Publisher before the first release.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Singulus
|
|
4
|
+
module Internal
|
|
5
|
+
class Configuration
|
|
6
|
+
MODES = %i[standard strict runtime].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :default_mode
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
self.default_mode = :strict
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def default_mode=(mode)
|
|
15
|
+
mode = normalize_mode(mode)
|
|
16
|
+
|
|
17
|
+
unless MODES.include?(mode)
|
|
18
|
+
raise InvalidModeError,
|
|
19
|
+
"invalid Singulus mode #{mode.inspect}; expected one of: #{MODES.join(', ')}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@default_mode = mode
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def normalize_mode(mode)
|
|
28
|
+
mode.to_sym
|
|
29
|
+
rescue NoMethodError
|
|
30
|
+
mode
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Singulus
|
|
4
|
+
module Internal
|
|
5
|
+
module ConstructorGuard
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def new(...)
|
|
9
|
+
singulus_reject_unauthorized_constructor!(:new)
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def allocate
|
|
14
|
+
singulus_reject_unauthorized_constructor!(:allocate)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def singulus_reject_unauthorized_constructor!(method_name)
|
|
19
|
+
return unless Internal.locally_hardened?(self)
|
|
20
|
+
return if Internal.constructor_access_allowed?(self)
|
|
21
|
+
|
|
22
|
+
raise ConstructorAccessError,
|
|
23
|
+
"constructor #{method_name.inspect} is inaccessible for #{Internal.kind_for(self)} #{self}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Singulus
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
module Internal
|
|
7
|
+
class ConstructorAccessError < Singulus::Error; end
|
|
8
|
+
class DuplicationError < Singulus::Error; end
|
|
9
|
+
class InheritanceError < Singulus::Error; end
|
|
10
|
+
class InvalidModeError < Singulus::Error; end
|
|
11
|
+
class InvalidRetentionError < Singulus::Error; end
|
|
12
|
+
class ConfigurationLockedError < Singulus::Error; end
|
|
13
|
+
class RecursiveInitializationError < Singulus::Error; end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Singulus
|
|
4
|
+
module Internal
|
|
5
|
+
module InstanceGuard
|
|
6
|
+
def dup
|
|
7
|
+
singulus_reject_duplication!(:duplicated)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def clone(**options)
|
|
12
|
+
singulus_reject_duplication!(:cloned)
|
|
13
|
+
return super() if options.empty?
|
|
14
|
+
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize_dup(other)
|
|
19
|
+
singulus_reject_duplication!(:duplicated)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize_clone(other, **kwargs)
|
|
24
|
+
singulus_reject_duplication!(:cloned)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def singulus_reject_duplication!(operation)
|
|
31
|
+
return unless Internal.locally_hardened?(self.class)
|
|
32
|
+
|
|
33
|
+
raise DuplicationError, "#{self.class} instance cannot be #{operation} in hardened mode"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Singulus
|
|
4
|
+
module Internal
|
|
5
|
+
module MutationGuard
|
|
6
|
+
def singleton_method_added(method_name)
|
|
7
|
+
return super if @__singulus_restoring_constructor__
|
|
8
|
+
return super unless Internal.locally_hardened?(self)
|
|
9
|
+
return super unless Internal.constructor_name?(method_name)
|
|
10
|
+
|
|
11
|
+
singulus_restore_constructor!(method_name)
|
|
12
|
+
|
|
13
|
+
raise ConstructorAccessError,
|
|
14
|
+
"constructor #{method_name.inspect} cannot be redefined on #{Internal.kind_for(self)} #{self}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def singulus_restore_constructor!(method_name)
|
|
20
|
+
@__singulus_restoring_constructor__ = true
|
|
21
|
+
singleton_class.__send__(:remove_method, method_name)
|
|
22
|
+
|
|
23
|
+
if Internal.sealed_singleton?(self)
|
|
24
|
+
singleton_class.__send__(:undef_method, method_name)
|
|
25
|
+
else
|
|
26
|
+
private_class_method method_name
|
|
27
|
+
end
|
|
28
|
+
ensure
|
|
29
|
+
@__singulus_restoring_constructor__ = false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Singulus
|
|
4
|
+
module Internal
|
|
5
|
+
module ReflectionGuard
|
|
6
|
+
def method(method_name)
|
|
7
|
+
singulus_reject_constructor_access!(method_name)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def public_method(method_name)
|
|
12
|
+
singulus_reject_constructor_access!(method_name)
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def singleton_method(method_name)
|
|
17
|
+
singulus_reject_constructor_access!(method_name)
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def send(method_name, ...)
|
|
22
|
+
singulus_reject_constructor_access!(method_name)
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def __send__(method_name, ...)
|
|
27
|
+
singulus_reject_constructor_access!(method_name)
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def public_send(method_name, ...)
|
|
32
|
+
singulus_reject_constructor_access!(method_name)
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def singulus_reject_constructor_access!(method_name)
|
|
39
|
+
return unless Internal.locally_hardened?(self)
|
|
40
|
+
return unless Internal.constructor_name?(method_name)
|
|
41
|
+
|
|
42
|
+
raise ConstructorAccessError,
|
|
43
|
+
"constructor #{method_name.inspect} is inaccessible for #{Internal.kind_for(self)} #{self}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Singulus
|
|
4
|
+
module Internal
|
|
5
|
+
module RuntimeHardening
|
|
6
|
+
REFLECTION_GATEWAYS = %i[
|
|
7
|
+
method
|
|
8
|
+
public_method
|
|
9
|
+
singleton_method
|
|
10
|
+
send
|
|
11
|
+
__send__
|
|
12
|
+
public_send
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
module UnboundMethodGuard
|
|
16
|
+
def bind(object)
|
|
17
|
+
singulus_reject_binding!(object)
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def bind_call(object, *args, &)
|
|
22
|
+
singulus_reject_bind_call!(object, args)
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def singulus_reject_binding!(object)
|
|
29
|
+
return unless Internal.runtime_hardened?(object)
|
|
30
|
+
return unless Internal.constructor_name?(name) || RuntimeHardening.reflection_gateway?(name)
|
|
31
|
+
|
|
32
|
+
raise ConstructorAccessError,
|
|
33
|
+
"cannot bind #{owner}##{name} to runtime-hardened Singulus #{object}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def singulus_reject_bind_call!(object, args)
|
|
37
|
+
return unless Internal.runtime_hardened?(object)
|
|
38
|
+
|
|
39
|
+
if Internal.constructor_name?(name)
|
|
40
|
+
raise ConstructorAccessError,
|
|
41
|
+
"cannot invoke constructor #{owner}##{name} on runtime-hardened Singulus #{object}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return unless RuntimeHardening.reflection_gateway?(name)
|
|
45
|
+
return unless Internal.constructor_name?(args.first)
|
|
46
|
+
|
|
47
|
+
raise ConstructorAccessError,
|
|
48
|
+
"cannot use #{owner}##{name} to access constructor #{args.first.inspect} " \
|
|
49
|
+
"on runtime-hardened Singulus #{object}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module MethodGuard
|
|
54
|
+
def call(*args, &)
|
|
55
|
+
singulus_reject_invocation!(args)
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def [](*args, &)
|
|
60
|
+
singulus_reject_invocation!(args)
|
|
61
|
+
super
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_proc
|
|
65
|
+
singulus_reject_transformation!
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def >>(other)
|
|
70
|
+
singulus_reject_transformation!
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def <<(other)
|
|
75
|
+
singulus_reject_transformation!
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def singulus_reject_invocation!(args)
|
|
82
|
+
return unless Internal.runtime_hardened?(receiver)
|
|
83
|
+
|
|
84
|
+
if Internal.constructor_name?(name)
|
|
85
|
+
raise ConstructorAccessError,
|
|
86
|
+
"constructor #{name.inspect} cannot be invoked for runtime-hardened Singulus #{receiver}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return unless RuntimeHardening.reflection_gateway?(name)
|
|
90
|
+
return unless Internal.constructor_name?(args.first)
|
|
91
|
+
|
|
92
|
+
raise ConstructorAccessError,
|
|
93
|
+
"#{name} cannot be used to access constructor #{args.first.inspect} " \
|
|
94
|
+
"for runtime-hardened Singulus #{receiver}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def singulus_reject_transformation!
|
|
98
|
+
return unless Internal.runtime_hardened?(receiver)
|
|
99
|
+
return unless Internal.constructor_name?(name) || RuntimeHardening.reflection_gateway?(name)
|
|
100
|
+
|
|
101
|
+
raise ConstructorAccessError,
|
|
102
|
+
"Method #{name.inspect} for runtime-hardened Singulus #{receiver} " \
|
|
103
|
+
"cannot be converted or composed"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class << self
|
|
108
|
+
def enable!
|
|
109
|
+
mutex.synchronize do
|
|
110
|
+
return if enabled?
|
|
111
|
+
|
|
112
|
+
UnboundMethod.prepend(UnboundMethodGuard)
|
|
113
|
+
Method.prepend(MethodGuard)
|
|
114
|
+
|
|
115
|
+
@enabled = true
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def enabled?
|
|
120
|
+
@enabled == true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def reflection_gateway?(method_name)
|
|
124
|
+
REFLECTION_GATEWAYS.include?(Internal.normalize_method_name(method_name))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def mutex
|
|
130
|
+
@mutex ||= Thread::Mutex.new
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Singulus
|
|
4
|
+
module Internal
|
|
5
|
+
module SingletonClassMethods
|
|
6
|
+
def instance
|
|
7
|
+
return super unless Internal.locally_hardened?(self)
|
|
8
|
+
|
|
9
|
+
instance = Internal.with_constructor_access(self) { super }
|
|
10
|
+
singulus_seal_constructors!
|
|
11
|
+
instance
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def singulus(mode:)
|
|
15
|
+
Internal.set_mode!(self, mode)
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def singulus_mode
|
|
20
|
+
Internal.mode_for(self)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def inherited(subclass)
|
|
26
|
+
if Internal.locally_hardened?(self)
|
|
27
|
+
raise InheritanceError,
|
|
28
|
+
"#{self} is a Singulus in #{singulus_mode.inspect} mode and cannot be subclassed"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def singulus_seal_constructors!
|
|
35
|
+
return if singulus_sealed?
|
|
36
|
+
|
|
37
|
+
singulus_seal_mutex.synchronize do
|
|
38
|
+
return if singulus_sealed?
|
|
39
|
+
|
|
40
|
+
singulus_seal_constructor!(:new)
|
|
41
|
+
singulus_seal_constructor!(:allocate)
|
|
42
|
+
|
|
43
|
+
@__singulus_sealed__ = true
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def singulus_seal_constructor!(method_name)
|
|
48
|
+
singleton_class.__send__(:undef_method, method_name)
|
|
49
|
+
rescue NameError
|
|
50
|
+
return unless respond_to?(method_name, true)
|
|
51
|
+
|
|
52
|
+
raise
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def singulus_sealed?
|
|
56
|
+
@__singulus_sealed__ == true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def singulus_seal_mutex
|
|
60
|
+
@__singulus_seal_mutex__ ||= Thread::Mutex.new
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Singulus
|
|
4
|
+
module Multiton
|
|
5
|
+
RETENTIONS = %i[forever lru ttl bounded weak].freeze
|
|
6
|
+
private_constant :RETENTIONS
|
|
7
|
+
|
|
8
|
+
Entry = Struct.new(:value, :expires_at, keyword_init: true) do
|
|
9
|
+
def instance
|
|
10
|
+
value.is_a?(WeakRef) ? value.__getobj__ : value
|
|
11
|
+
rescue WeakRef::RefError
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def alive?
|
|
16
|
+
!instance.nil?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
private_constant :Entry
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def included(base)
|
|
23
|
+
install(base)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def with(mode = nil, **options)
|
|
27
|
+
resolved_mode = options.delete(:mode)
|
|
28
|
+
|
|
29
|
+
raise ArgumentError, "mode must be provided either positionally or as mode:, not both" if mode && resolved_mode
|
|
30
|
+
|
|
31
|
+
resolved_mode ||= mode || Singulus.configuration.default_mode
|
|
32
|
+
retention = options.delete(:retention)
|
|
33
|
+
ttl = options.delete(:ttl)
|
|
34
|
+
max_size = options.delete(:max_size)
|
|
35
|
+
|
|
36
|
+
raise ArgumentError, "unknown Multiton options: #{options.keys.map(&:inspect).join(', ')}" unless options.empty?
|
|
37
|
+
|
|
38
|
+
Module.new do
|
|
39
|
+
define_singleton_method(:included) do |base|
|
|
40
|
+
Multiton.install(base, mode: resolved_mode)
|
|
41
|
+
base.singulus(retention: retention, ttl: ttl, max_size: max_size) if retention
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def install(base, mode: Singulus.configuration.default_mode)
|
|
47
|
+
Internal.initialize_managed_class!(base, kind: :multiton, mode: mode)
|
|
48
|
+
|
|
49
|
+
base.include Internal::InstanceGuard
|
|
50
|
+
base.private_class_method :new, :allocate
|
|
51
|
+
base.singleton_class.prepend Internal::ReflectionGuard
|
|
52
|
+
base.singleton_class.prepend Internal::MutationGuard
|
|
53
|
+
base.singleton_class.prepend Internal::ConstructorGuard
|
|
54
|
+
base.extend ClassMethods
|
|
55
|
+
|
|
56
|
+
initialize_registry_state(base)
|
|
57
|
+
Internal::RuntimeHardening.enable! if Internal.runtime_hardened?(base)
|
|
58
|
+
base
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def initialize_registry_state(base)
|
|
64
|
+
base.instance_variable_set(:@__singulus_multiton_mutex__, Monitor.new)
|
|
65
|
+
base.instance_variable_set(:@__singulus_multiton_instances__, {})
|
|
66
|
+
base.instance_variable_set(:@__singulus_multiton_key_normalizer__, nil)
|
|
67
|
+
base.instance_variable_set(:@__singulus_multiton_retention__, :forever)
|
|
68
|
+
base.instance_variable_set(:@__singulus_multiton_ttl__, nil)
|
|
69
|
+
base.instance_variable_set(:@__singulus_multiton_max_size__, nil)
|
|
70
|
+
base.instance_variable_set(:@__singulus_multiton_initializing_keys__, {})
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
module ClassMethods
|
|
75
|
+
def instance_for(identifier, ...)
|
|
76
|
+
key = singulus_multiton_key_for(identifier)
|
|
77
|
+
|
|
78
|
+
singulus_multiton_mutex.synchronize do
|
|
79
|
+
singulus_multiton_purge_expired!
|
|
80
|
+
|
|
81
|
+
if (entry = singulus_multiton_instances[key])
|
|
82
|
+
if (instance = entry.instance)
|
|
83
|
+
singulus_multiton_touch_lru!(key, entry)
|
|
84
|
+
return instance
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
singulus_multiton_instances.delete(key)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
singulus_multiton_reject_recursive_initialization!(key)
|
|
91
|
+
singulus_multiton_initializing_keys[key] = true
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
instance = Internal.with_constructor_access(self) do
|
|
95
|
+
new(identifier, ...)
|
|
96
|
+
end
|
|
97
|
+
singulus_multiton_store!(key, instance)
|
|
98
|
+
instance
|
|
99
|
+
ensure
|
|
100
|
+
singulus_multiton_initializing_keys.delete(key)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def instance?(identifier)
|
|
106
|
+
key = singulus_multiton_key_for(identifier)
|
|
107
|
+
|
|
108
|
+
singulus_multiton_mutex.synchronize do
|
|
109
|
+
singulus_multiton_purge_expired!
|
|
110
|
+
singulus_multiton_instances.key?(key)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def delete_instance(identifier)
|
|
115
|
+
key = singulus_multiton_key_for(identifier)
|
|
116
|
+
|
|
117
|
+
singulus_multiton_mutex.synchronize do
|
|
118
|
+
singulus_multiton_purge_expired!
|
|
119
|
+
singulus_multiton_instances.delete(key)&.instance
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def clear_instances
|
|
124
|
+
singulus_multiton_mutex.synchronize do
|
|
125
|
+
singulus_multiton_purge_expired!
|
|
126
|
+
count = singulus_multiton_instances.length
|
|
127
|
+
singulus_multiton_instances.clear
|
|
128
|
+
count
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def instance_count
|
|
133
|
+
singulus_multiton_mutex.synchronize do
|
|
134
|
+
singulus_multiton_purge_expired!
|
|
135
|
+
singulus_multiton_instances.length
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def instance_keys
|
|
140
|
+
singulus_multiton_mutex.synchronize do
|
|
141
|
+
singulus_multiton_purge_expired!
|
|
142
|
+
singulus_multiton_instances.keys.dup.freeze
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def singulus(mode: nil, retention: nil, ttl: nil, max_size: nil)
|
|
147
|
+
Internal.set_mode!(self, mode) if mode
|
|
148
|
+
multiton_retention(retention, ttl: ttl, max_size: max_size) if retention
|
|
149
|
+
self
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def singulus_mode
|
|
153
|
+
Internal.mode_for(self)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def multiton_key(&block)
|
|
157
|
+
return @__singulus_multiton_key_normalizer__ unless block
|
|
158
|
+
|
|
159
|
+
singulus_multiton_assert_registry_empty!("key normalizer")
|
|
160
|
+
@__singulus_multiton_key_normalizer__ = block
|
|
161
|
+
self
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def multiton_retention(strategy = nil, ttl: nil, max_size: nil)
|
|
165
|
+
return @__singulus_multiton_retention__ unless strategy
|
|
166
|
+
|
|
167
|
+
strategy = Internal.normalize_method_name(strategy)
|
|
168
|
+
unless RETENTIONS.include?(strategy)
|
|
169
|
+
raise Internal::InvalidRetentionError,
|
|
170
|
+
"invalid Multiton retention #{strategy.inspect}; expected one of: #{RETENTIONS.join(', ')}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
singulus_multiton_validate_retention_options!(strategy, ttl: ttl, max_size: max_size)
|
|
174
|
+
singulus_multiton_assert_registry_empty!("retention")
|
|
175
|
+
|
|
176
|
+
@__singulus_multiton_retention__ = strategy
|
|
177
|
+
@__singulus_multiton_ttl__ = ttl&.to_f
|
|
178
|
+
@__singulus_multiton_max_size__ = max_size&.to_i
|
|
179
|
+
self
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def inherited(subclass)
|
|
185
|
+
if Internal.locally_hardened?(self)
|
|
186
|
+
raise Internal::InheritanceError,
|
|
187
|
+
"#{self} is a Multiton in #{singulus_mode.inspect} mode and cannot be subclassed"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
super
|
|
191
|
+
Multiton.install(subclass, mode: singulus_mode)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def singulus_multiton_instances
|
|
195
|
+
@__singulus_multiton_instances__ ||= {}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def singulus_multiton_mutex
|
|
199
|
+
@__singulus_multiton_mutex__ ||= Monitor.new
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def singulus_multiton_initializing_keys
|
|
203
|
+
@__singulus_multiton_initializing_keys__ ||= {}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def singulus_multiton_key_for(identifier)
|
|
207
|
+
normalizer = @__singulus_multiton_key_normalizer__
|
|
208
|
+
key = normalizer ? normalizer.call(identifier) : identifier
|
|
209
|
+
singulus_multiton_stabilize_key(key)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def singulus_multiton_stabilize_key(key)
|
|
213
|
+
case key
|
|
214
|
+
when String
|
|
215
|
+
key.dup.freeze
|
|
216
|
+
when Array
|
|
217
|
+
key.map { |item| singulus_multiton_stabilize_key(item) }.freeze
|
|
218
|
+
when Hash
|
|
219
|
+
key.each_with_object({}) do |(hash_key, value), stabilized|
|
|
220
|
+
stabilized[singulus_multiton_stabilize_key(hash_key)] = singulus_multiton_stabilize_key(value)
|
|
221
|
+
end.freeze
|
|
222
|
+
else
|
|
223
|
+
key
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def singulus_multiton_store!(key, instance)
|
|
228
|
+
retention = @__singulus_multiton_retention__
|
|
229
|
+
ttl_retention = %i[ttl bounded].include?(retention)
|
|
230
|
+
expires_at = singulus_multiton_monotonic_time + @__singulus_multiton_ttl__ if ttl_retention
|
|
231
|
+
value = retention == :weak ? WeakRef.new(instance) : instance
|
|
232
|
+
|
|
233
|
+
singulus_multiton_instances[key] = Entry.new(value: value, expires_at: expires_at)
|
|
234
|
+
singulus_multiton_evict_lru! if %i[lru bounded].include?(retention)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def singulus_multiton_touch_lru!(key, entry)
|
|
238
|
+
return unless %i[lru bounded].include?(@__singulus_multiton_retention__)
|
|
239
|
+
|
|
240
|
+
singulus_multiton_instances.delete(key)
|
|
241
|
+
singulus_multiton_instances[key] = entry
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def singulus_multiton_evict_lru!
|
|
245
|
+
max_size = @__singulus_multiton_max_size__
|
|
246
|
+
singulus_multiton_instances.shift while singulus_multiton_instances.length > max_size
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def singulus_multiton_purge_expired!
|
|
250
|
+
retention = @__singulus_multiton_retention__
|
|
251
|
+
|
|
252
|
+
if %i[ttl bounded].include?(retention)
|
|
253
|
+
now = singulus_multiton_monotonic_time
|
|
254
|
+
singulus_multiton_instances.delete_if { |_key, entry| entry.expires_at <= now }
|
|
255
|
+
elsif retention == :weak
|
|
256
|
+
singulus_multiton_instances.delete_if { |_key, entry| !entry.alive? }
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def singulus_multiton_monotonic_time
|
|
261
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def singulus_multiton_validate_retention_options!(strategy, ttl:, max_size:)
|
|
265
|
+
case strategy
|
|
266
|
+
when :forever, :weak
|
|
267
|
+
return unless ttl || max_size
|
|
268
|
+
|
|
269
|
+
raise Internal::InvalidRetentionError,
|
|
270
|
+
"#{strategy.inspect} retention does not accept ttl: or max_size:"
|
|
271
|
+
when :ttl
|
|
272
|
+
if !ttl || ttl.to_f <= 0
|
|
273
|
+
raise Internal::InvalidRetentionError,
|
|
274
|
+
":ttl retention requires ttl: greater than zero"
|
|
275
|
+
end
|
|
276
|
+
return unless max_size
|
|
277
|
+
|
|
278
|
+
raise Internal::InvalidRetentionError,
|
|
279
|
+
"max_size: is not valid for :ttl retention; use retention: :bounded to combine ttl and max_size"
|
|
280
|
+
when :lru
|
|
281
|
+
if !max_size || max_size.to_i <= 0
|
|
282
|
+
raise Internal::InvalidRetentionError, ":lru retention requires max_size: greater than zero"
|
|
283
|
+
end
|
|
284
|
+
return unless ttl
|
|
285
|
+
|
|
286
|
+
raise Internal::InvalidRetentionError,
|
|
287
|
+
"ttl: is not valid for :lru retention; use retention: :bounded to combine ttl and max_size"
|
|
288
|
+
when :bounded
|
|
289
|
+
if !ttl || ttl.to_f <= 0
|
|
290
|
+
raise Internal::InvalidRetentionError,
|
|
291
|
+
":bounded retention requires ttl: greater than zero"
|
|
292
|
+
end
|
|
293
|
+
return if max_size&.to_i&.positive?
|
|
294
|
+
|
|
295
|
+
raise Internal::InvalidRetentionError, ":bounded retention requires max_size: greater than zero"
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def singulus_multiton_reject_recursive_initialization!(key)
|
|
300
|
+
return unless singulus_multiton_initializing_keys.key?(key)
|
|
301
|
+
|
|
302
|
+
raise Internal::RecursiveInitializationError,
|
|
303
|
+
"recursive Multiton initialization detected for key #{key.inspect} on #{self}"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def singulus_multiton_assert_registry_empty!(setting)
|
|
307
|
+
return if instance_count.zero?
|
|
308
|
+
|
|
309
|
+
raise Internal::ConfigurationLockedError,
|
|
310
|
+
"cannot change Multiton #{setting} after instances have been created; clear_instances first"
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
private_constant :ClassMethods
|
|
315
|
+
end
|
|
316
|
+
end
|
data/lib/singulus.rb
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "monitor"
|
|
5
|
+
require "weakref"
|
|
6
|
+
|
|
7
|
+
require_relative "singulus/version"
|
|
8
|
+
require_relative "singulus/internal/errors"
|
|
9
|
+
require_relative "singulus/internal/configuration"
|
|
10
|
+
require_relative "singulus/internal/instance_guard"
|
|
11
|
+
require_relative "singulus/internal/constructor_guard"
|
|
12
|
+
require_relative "singulus/internal/reflection_guard"
|
|
13
|
+
require_relative "singulus/internal/mutation_guard"
|
|
14
|
+
require_relative "singulus/internal/singleton_class_methods"
|
|
15
|
+
require_relative "singulus/internal/runtime_hardening"
|
|
16
|
+
require_relative "singulus/multiton"
|
|
17
|
+
|
|
18
|
+
module Singulus
|
|
19
|
+
module Internal
|
|
20
|
+
CONSTRUCTORS = %i[new allocate].freeze
|
|
21
|
+
|
|
22
|
+
MARKER_IVAR = :@__singulus_managed_class__
|
|
23
|
+
KIND_IVAR = :@__singulus_kind__
|
|
24
|
+
MODE_IVAR = :@__singulus_mode__
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def install_singleton!(base, mode: Singulus.configuration.default_mode)
|
|
28
|
+
base.include ::Singleton
|
|
29
|
+
base.include InstanceGuard
|
|
30
|
+
|
|
31
|
+
initialize_managed_class!(base, kind: :singleton, mode: mode)
|
|
32
|
+
base.instance_variable_set(:@__singulus_seal_mutex__, Thread::Mutex.new)
|
|
33
|
+
base.instance_variable_set(:@__singulus_sealed__, false)
|
|
34
|
+
|
|
35
|
+
base.singleton_class.prepend ReflectionGuard
|
|
36
|
+
base.singleton_class.prepend MutationGuard
|
|
37
|
+
base.singleton_class.prepend ConstructorGuard
|
|
38
|
+
base.extend SingletonClassMethods
|
|
39
|
+
|
|
40
|
+
RuntimeHardening.enable! if runtime_hardened?(base)
|
|
41
|
+
base
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize_managed_class!(base, kind:, mode:)
|
|
45
|
+
base.instance_variable_set(MARKER_IVAR, true)
|
|
46
|
+
base.instance_variable_set(KIND_IVAR, kind)
|
|
47
|
+
base.instance_variable_set(MODE_IVAR, normalize_mode!(mode))
|
|
48
|
+
base
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def managed_class?(object)
|
|
52
|
+
object.is_a?(Class) && object.instance_variable_get(MARKER_IVAR) == true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
alias singulus_class? managed_class?
|
|
56
|
+
|
|
57
|
+
def kind_for(klass)
|
|
58
|
+
return unless managed_class?(klass)
|
|
59
|
+
|
|
60
|
+
klass.instance_variable_get(KIND_IVAR)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def mode_for(klass)
|
|
64
|
+
return unless managed_class?(klass)
|
|
65
|
+
|
|
66
|
+
klass.instance_variable_get(MODE_IVAR)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def set_mode!(klass, mode)
|
|
70
|
+
raise ArgumentError, "#{klass} is not managed by Singulus" unless managed_class?(klass)
|
|
71
|
+
|
|
72
|
+
normalized_mode = normalize_mode!(mode)
|
|
73
|
+
current_mode = mode_for(klass)
|
|
74
|
+
|
|
75
|
+
if kind_for(klass) == :singleton && current_mode != :standard && normalized_mode == :standard &&
|
|
76
|
+
klass.instance_variable_get(:@__singulus_sealed__)
|
|
77
|
+
raise InvalidModeError,
|
|
78
|
+
"cannot downgrade #{klass} to :standard after its constructors have been sealed"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
klass.instance_variable_set(MODE_IVAR, normalized_mode)
|
|
82
|
+
RuntimeHardening.enable! if normalized_mode == :runtime
|
|
83
|
+
normalized_mode
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def locally_hardened?(klass)
|
|
87
|
+
%i[strict runtime].include?(mode_for(klass))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def runtime_hardened?(klass)
|
|
91
|
+
mode_for(klass) == :runtime
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def sealed_singleton?(klass)
|
|
95
|
+
kind_for(klass) == :singleton && klass.instance_variable_get(:@__singulus_sealed__) == true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def constructor_name?(method_name)
|
|
99
|
+
CONSTRUCTORS.include?(normalize_method_name(method_name))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def normalize_method_name(method_name)
|
|
103
|
+
method_name.to_sym
|
|
104
|
+
rescue NoMethodError
|
|
105
|
+
method_name
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def constructor_access_allowed?(klass)
|
|
109
|
+
permissions = Thread.current[:__singulus_constructor_permissions__]
|
|
110
|
+
permissions && permissions[klass].to_i.positive?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def with_constructor_access(klass)
|
|
114
|
+
permissions = (Thread.current[:__singulus_constructor_permissions__] ||= {})
|
|
115
|
+
permissions[klass] = permissions[klass].to_i + 1
|
|
116
|
+
yield
|
|
117
|
+
ensure
|
|
118
|
+
if permissions
|
|
119
|
+
permissions[klass] = permissions[klass].to_i - 1
|
|
120
|
+
permissions.delete(klass) unless permissions[klass].to_i.positive?
|
|
121
|
+
Thread.current[:__singulus_constructor_permissions__] = nil if permissions.empty?
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def normalize_mode!(mode)
|
|
128
|
+
normalized_mode = normalize_method_name(mode)
|
|
129
|
+
return normalized_mode if Configuration::MODES.include?(normalized_mode)
|
|
130
|
+
|
|
131
|
+
raise InvalidModeError,
|
|
132
|
+
"invalid Singulus mode #{mode.inspect}; expected one of: #{Configuration::MODES.join(', ')}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private_constant :Internal
|
|
138
|
+
|
|
139
|
+
class << self
|
|
140
|
+
def configuration
|
|
141
|
+
@configuration ||= Internal::Configuration.new
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def configure
|
|
145
|
+
raise ArgumentError, "a block is required" unless block_given?
|
|
146
|
+
|
|
147
|
+
yield configuration
|
|
148
|
+
Internal::RuntimeHardening.enable! if configuration.default_mode == :runtime
|
|
149
|
+
configuration
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def reset_configuration!
|
|
153
|
+
@configuration = Internal::Configuration.new
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
module Singleton
|
|
158
|
+
class << self
|
|
159
|
+
def included(base)
|
|
160
|
+
Internal.install_singleton!(base)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def with(mode = nil, **options)
|
|
164
|
+
resolved_mode = options.delete(:mode)
|
|
165
|
+
|
|
166
|
+
raise ArgumentError, "mode must be provided either positionally or as mode:, not both" if mode && resolved_mode
|
|
167
|
+
|
|
168
|
+
unless options.empty?
|
|
169
|
+
raise ArgumentError, "unknown Singleton options: #{options.keys.map(&:inspect).join(', ')}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
resolved_mode ||= mode || Singulus.configuration.default_mode
|
|
173
|
+
|
|
174
|
+
Module.new do
|
|
175
|
+
define_singleton_method(:included) do |base|
|
|
176
|
+
Internal.install_singleton!(base, mode: resolved_mode)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: singulus
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Juan Furattini
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |-
|
|
13
|
+
Singulus provides configurable local and runtime hardening for classic
|
|
14
|
+
Singleton and keyed Multiton patterns, including reflection guards, constructor
|
|
15
|
+
protection, mutation protection, inheritance restrictions, bounded retention
|
|
16
|
+
strategies, and optional guards around Method and UnboundMethod.
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE.txt
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/singulus.rb
|
|
25
|
+
- lib/singulus/internal/configuration.rb
|
|
26
|
+
- lib/singulus/internal/constructor_guard.rb
|
|
27
|
+
- lib/singulus/internal/errors.rb
|
|
28
|
+
- lib/singulus/internal/instance_guard.rb
|
|
29
|
+
- lib/singulus/internal/mutation_guard.rb
|
|
30
|
+
- lib/singulus/internal/reflection_guard.rb
|
|
31
|
+
- lib/singulus/internal/runtime_hardening.rb
|
|
32
|
+
- lib/singulus/internal/singleton_class_methods.rb
|
|
33
|
+
- lib/singulus/multiton.rb
|
|
34
|
+
- lib/singulus/version.rb
|
|
35
|
+
homepage: https://github.com/Rubcraft/singulus
|
|
36
|
+
licenses:
|
|
37
|
+
- MIT
|
|
38
|
+
metadata:
|
|
39
|
+
allowed_push_host: https://rubygems.org
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
41
|
+
source_code_uri: https://github.com/Rubcraft/singulus
|
|
42
|
+
changelog_uri: https://github.com/Rubcraft/singulus/blob/main/CHANGELOG.md
|
|
43
|
+
rdoc_options: []
|
|
44
|
+
require_paths:
|
|
45
|
+
- lib
|
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '3.2'
|
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
requirements: []
|
|
57
|
+
rubygems_version: 4.0.16
|
|
58
|
+
specification_version: 4
|
|
59
|
+
summary: Hardened Singleton and keyed Multiton patterns for Ruby
|
|
60
|
+
test_files: []
|