placement_machines 0.1.4-arm64-darwin

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b287ef12e08de1427016d42d4e93550be27af3a00111e7e4d3508bf1d8bdc2db
4
+ data.tar.gz: b73697165dc6dcd68e8bb9aee4b8af647b11357b752dbde0805afce454cd0656
5
+ SHA512:
6
+ metadata.gz: 836f25f4315eb629526426942f8610b147c4bf38bd1fe80cc10cd67ec45ac6061f9c0ec2ca023ed64c5690815e4bf069a8f4c14ae5226f3ae79f6a40121ff112
7
+ data.tar.gz: 4a722ddef3c2e17c20930a70f2c375b3c5f7e0e923e31de71649c00ef06069865454226cc37e239aadbe016cd0240fe2292420d7d72bd669f9895f80d250151f
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [0.1.4](https://github.com/seuros/placement_machines/compare/placement_machines/v0.1.3...placement_machines/v0.1.4) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * crates.io keywords and categories for the core ([997f0d7](https://github.com/seuros/placement_machines/commit/997f0d70e6292afdf3212730e3a1ac2e4662b86e))
9
+
10
+ ## [0.1.3](https://github.com/seuros/placement_machines/compare/placement_machines/v0.1.2...placement_machines/v0.1.3) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * keep test sources out of the packaged gem and crate ([18a75e8](https://github.com/seuros/placement_machines/commit/18a75e8cdf890fcff09732c53191b957db3fd43d))
16
+
17
+ ## [0.1.2](https://github.com/seuros/placement_machines/compare/placement_machines/v0.1.1...placement_machines/v0.1.2) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * cross builds no longer compile for the host ([3218c79](https://github.com/seuros/placement_machines/commit/3218c795d9cd67f08b6d881d53c2488c37a5e2bd))
23
+
24
+ ## [0.1.1](https://github.com/seuros/placement_machines/compare/placement_machines-v0.1.0...placement_machines/v0.1.1) (2026-11-06)
25
+
26
+
27
+ ### Features
28
+
29
+ * initial extraction ([eb26a4d](https://github.com/seuros/placement_machines/commit/eb26a4d14c4ee79cbd0f6afc8edbd8b55e598bae))
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdelkader Boudih
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,185 @@
1
+ # PlacementMachines
2
+
3
+ > When one station goes dark, only its freight moves. 🧭
4
+
5
+ Consistent placement for Ruby. Give it a key and the list of nodes you have right now, and it tells you which node owns the key. Add a node, lose a node, ask again, and only the keys that belonged to the changed node get a new answer. Everything else stays exactly where it was. It can also pick three replicas spread across failure domains, and it can walk a hash ring if you already have one.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem 'placement_machines'
11
+ ```
12
+
13
+ ```ruby
14
+ # [node_id, failure_domain] for every node currently alive
15
+ nodes = [[10, 1], [20, 1], [30, 2], [40, 2]]
16
+
17
+ PlacementMachines.rendezvous_owner(42, nodes) # => 30
18
+ PlacementMachines.replicas(42, nodes) # => [30, 20, nil]
19
+ ```
20
+
21
+ Node 30 owns key 42. If you want copies, the second goes to node 20, in a different domain. The third slot is `nil` because there are only two domains and the gem refuses to put two copies in the same one.
22
+
23
+ ## A Message to the Resistance
24
+
25
+ So you sharded the cache. `node = nodes[key.hash % nodes.size]`. Four nodes, keys spread evenly, latency graphs flat, everyone goes home.
26
+
27
+ Then node 30 reboots for a kernel patch. `nodes.size` goes from 4 to 3. Every single key gets a new modulus, which means three out of four keys now point at a node that has never heard of them. The cache hit rate drops to the floor, the database behind it catches the full read load, and the on-call engineer learns what a thundering herd feels like at 2 AM. Over one node. That was patched. On purpose.
28
+
29
+ The fix is not a smarter modulus. The fix is to score every node for every key independently, so removing one node only disturbs the keys that scored highest on that node. Those keys move. Nothing else does. That is what this gem computes, and it does it the same way on every machine that asks, with no coordination and nothing to sync.
30
+
31
+ ## Chapter 1: Stardate 2025.186, the AMS Karger
32
+
33
+ > **AMS Karger**, NCC-4213, *Karger*-class navigation tender.
34
+ > A missing station should not require redrawing the entire galaxy.
35
+
36
+ The tender holds no freight and commands no ships. It answers one question for any manifest and any list of stations: where does this go? Ask it on any bridge in the fleet with the same station list and you get the same answer. Strike a station from the list and the only manifests that get a new destination are the ones that were bound for it.
37
+
38
+ Somewhere in the fleet, a cadet proposes:
39
+
40
+ ```ruby
41
+ def station_for(manifest)
42
+ STATIONS[manifest.id % STATIONS.size]
43
+ end
44
+ ```
45
+
46
+ "That," says the navigator, "is how one dark station reroutes the whole galaxy."
47
+
48
+ ## The Flight Plan
49
+
50
+ Here is the life of one key:
51
+
52
+ ```mermaid
53
+ stateDiagram-v2
54
+ [*] --> owned_by_30: rendezvous_owner(42, nodes)
55
+ owned_by_30 --> owned_by_30: some other node joins or leaves
56
+ owned_by_30 --> owned_by_20: node 30 leaves, next-best score wins
57
+ owned_by_20 --> owned_by_30: node 30 returns, its score still wins
58
+ ```
59
+
60
+ And the night node 30 went down for a patch:
61
+
62
+ ```mermaid
63
+ sequenceDiagram
64
+ participant App
65
+ participant P as PlacementMachines
66
+ participant N30 as Node 30
67
+ participant N20 as Node 20
68
+
69
+ App->>P: rendezvous_owner(42, [10, 20, 30, 40])
70
+ P-->>App: 30
71
+ App->>N30: write key 42
72
+ Note over N30: reboots for a kernel patch
73
+
74
+ App->>P: rendezvous_owner(42, [10, 20, 40])
75
+ P-->>App: 20
76
+ App->>N20: write key 42
77
+ Note over App: keys owned by 10, 20 and 40 never moved
78
+
79
+ Note over N30: back online
80
+ App->>P: rendezvous_owner(42, [10, 20, 30, 40])
81
+ P-->>App: 30
82
+ ```
83
+
84
+ ## How Placement Works
85
+
86
+ ### Rendezvous
87
+
88
+ Every node gets a score for every key. Highest score owns the key. The score depends only on the key and the node id, never on who else is in the list.
89
+
90
+ ```ruby
91
+ nodes = [[10, 1], [20, 1], [30, 2], [40, 2]]
92
+
93
+ PlacementMachines.rendezvous_owner(42, nodes) # => 30
94
+ PlacementMachines.rendezvous_owner(42, nodes.reject { |id, _| id == 30 }) # => 20
95
+ ```
96
+
97
+ Node 30 drops out and key 42 goes to node 20, the runner-up. Measure it over a thousand keys and the point lands:
98
+
99
+ ```ruby
100
+ before = nodes
101
+ after = nodes.reject { |id, _| id == 30 }
102
+
103
+ moved = (0...1000).count do |key|
104
+ PlacementMachines.rendezvous_owner(key, before) != PlacementMachines.rendezvous_owner(key, after)
105
+ end
106
+ owned_by_30 = (0...1000).count { |key| PlacementMachines.rendezvous_owner(key, before) == 30 }
107
+
108
+ moved == owned_by_30 # => true
109
+ ```
110
+
111
+ Only node 30's keys moved. Compare that with `key % 4` becoming `key % 3`, which reassigns roughly three quarters of them.
112
+
113
+ The scores are available directly if you want to rank nodes yourself:
114
+
115
+ ```ruby
116
+ PlacementMachines.score(42, 30) # => a large Integer
117
+ PlacementMachines.score(42, 30) > PlacementMachines.score(42, 20) # => true
118
+ ```
119
+
120
+ Larger wins. Equal scores go to the lower node id, so there is never a tie.
121
+
122
+ ### Replicas across failure domains
123
+
124
+ The second number on each node is its failure domain: a rack, an availability zone, a power circuit, whatever you don't want to lose twice. `replicas` returns three slots, highest score first, and skips any node whose domain is already represented.
125
+
126
+ ```ruby
127
+ nodes = [[10, 1], [20, 1], [30, 2], [40, 2]]
128
+ PlacementMachines.replicas(42, nodes) # => [30, 20, nil]
129
+
130
+ PlacementMachines.replicas(7, [[10, 1]]) # => [10, nil, nil]
131
+ ```
132
+
133
+ Two domains, two replicas, one empty slot. One node, one replica, two empty slots. The gem never fills a slot with a node that doesn't exist or a node that shares a domain with an earlier pick. An empty slot is information: you are under-replicated, and now you know by how much.
134
+
135
+ The first replica is always the rendezvous owner, so the two calls agree.
136
+
137
+ ### The ring
138
+
139
+ If you already run a hash ring with virtual nodes, hand it the points. Each point is `[position, node_id]`, in any order. The owner is the first point clockwise from the key, wrapping around at the top.
140
+
141
+ ```ruby
142
+ ring = [[100, 1], [500, 2], [900, 3]]
143
+
144
+ PlacementMachines.ring_owner(250, ring) # => 2
145
+ PlacementMachines.ring_owner(500, ring) # => 2, exact hit
146
+ PlacementMachines.ring_owner(950, ring) # => 1, wrapped around
147
+ ```
148
+
149
+ A node may appear at many positions. Two points on the same position go to the lower node id.
150
+
151
+ ### Nothing to place
152
+
153
+ No nodes means no owner, not an error:
154
+
155
+ ```ruby
156
+ PlacementMachines.rendezvous_owner(1, []) # => nil
157
+ PlacementMachines.replicas(1, []) # => [nil, nil, nil]
158
+ PlacementMachines.ring_owner(1, []) # => nil
159
+ ```
160
+
161
+ ## The Rules of the Tender
162
+
163
+ - **Everything is an Integer** in `0..2**64-1`: keys, node ids, domains, ring positions. Floats raise `TypeError`, negatives and overflow raise `RangeError`. Objects that merely respond to `to_int` are not integers.
164
+ - **Nodes are `[id, domain]` pairs**, ring points are `[position, id]` pairs, each a real two-element Array. Anything else raises.
165
+ - **Node ids are unique.** The same id twice in one list raises `ArgumentError`, even with different domains.
166
+ - **Lists have ceilings.** At most 64 nodes and 1024 ring points per call. Over that raises `ArgumentError`.
167
+ - **Inputs are never touched.** Pass frozen arrays if you like. Results are fresh arrays you own.
168
+ - **Three replicas, always three slots.** The count is fixed and unfilled slots are `nil`.
169
+
170
+ ## What You Think You're Doing vs Reality
171
+
172
+ ### You Think: "Three replicas means I can lose two nodes."
173
+ ### Reality: Three replicas on one rack means you can lose one power strip.
174
+
175
+ Spreading copies across nodes protects you from a node dying. It does nothing when the thing that dies is the rack, the zone, or the circuit all three nodes share. The domain is how you tell the placer what fails together. Give every node the same domain and `replicas` will honestly hand you one copy and two empty slots, because that is what you actually have.
176
+
177
+ ## What's Yours
178
+
179
+ The placer decides where a key should live. It does not know which nodes are alive, so the list you pass in has to be your real, current membership, and every machine that calls it has to pass the same list or they will disagree. Moving data when ownership changes, repairing replicas, and choosing how keys and ring positions are hashed are all yours.
180
+
181
+ ## License
182
+
183
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
184
+
185
+ **Remember: `key % nodes.size` is not a placement strategy. It's a scheduled outage with extra steps.**
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## [0.2.2](https://github.com/seuros/placement_machines/compare/placement-machines-v0.2.1...placement-machines-v0.2.2) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * crates.io keywords and categories for the core ([997f0d7](https://github.com/seuros/placement_machines/commit/997f0d70e6292afdf3212730e3a1ac2e4662b86e))
9
+
10
+ ## [0.2.1](https://github.com/seuros/placement_machines/compare/placement-machines-v0.2.0...placement-machines-v0.2.1) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * keep test sources out of the packaged gem and crate ([18a75e8](https://github.com/seuros/placement_machines/commit/18a75e8cdf890fcff09732c53191b957db3fd43d))
16
+
17
+ ## [0.2.0](https://github.com/seuros/placement_machines/compare/placement-machines-v0.1.0...placement-machines-v0.2.0) (2026-11-06)
18
+
19
+
20
+ ### Features
21
+
22
+ * initial extraction ([eb26a4d](https://github.com/seuros/placement_machines/commit/eb26a4d14c4ee79cbd0f6afc8edbd8b55e598bae))
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdelkader Boudih
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.
@@ -0,0 +1,33 @@
1
+ # placement-machines
2
+
3
+ > **AMS Karger**, NCC-4213, *Karger*-class navigation tender.
4
+
5
+ Engine-room flight manual: chart deterministic destinations and replica berths.
6
+
7
+ Allocation-free `no_std` placement over stable caller-supplied numeric key hashes
8
+ and node identities. Rendezvous scores use a documented SplitMix64-style mixer,
9
+ not a cryptographic hash. Do not use these scores for adversarial admission or
10
+ security decisions. Ties favor lower node IDs, independent of input order.
11
+
12
+ Replica selection requires unique node IDs and chooses at most one node in each
13
+ failure domain. Missing replicas are explicit `None` values, never repeated nodes.
14
+ Weights, fallback within an exhausted domain, and topology hierarchies are not
15
+ implemented. Ring lookup accepts caller-generated virtual points, including
16
+ wraparound; ring construction and hash-version agreement are external contracts.
17
+ Placement does not define replica membership changes or consensus quorums.
18
+
19
+ ```rust
20
+ use placement_machines::{Node, NodeId, replicas};
21
+ let nodes = [
22
+ Node { id: NodeId(1), domain: 10 },
23
+ Node { id: NodeId(2), domain: 20 },
24
+ ];
25
+ # fn example(nodes: &[Node]) -> Option<()> {
26
+ let selected = replicas::<2>(42, nodes)?;
27
+ assert!(selected[0].is_some());
28
+ assert!(selected[1].is_some());
29
+ assert_ne!(selected[0], selected[1]);
30
+ # Some(())
31
+ # }
32
+ # assert_eq!(example(&nodes), Some(()));
33
+ ```
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ load File.expand_path('ffi/extconf.rb', __dir__)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlacementMachines
4
+ VERSION = '0.1.4'
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'placement_machines/version'
4
+
5
+ # There is no pure-Ruby policy fallback when the native extension is missing.
6
+ require 'placement_machines_native/placement_machines_native'
@@ -0,0 +1,11 @@
1
+ module PlacementMachines
2
+ VERSION: String
3
+ REPLICA_COUNT: Integer
4
+ MAX_NODES: Integer
5
+ MAX_RING_POINTS: Integer
6
+
7
+ def self.score: (Integer key, Integer node_id) -> Integer
8
+ def self.replicas: (Integer key, Array[[Integer, Integer]] nodes) -> Array[Integer?]
9
+ def self.rendezvous_owner: (Integer key, Array[[Integer, Integer]] nodes) -> Integer?
10
+ def self.ring_owner: (Integer key, Array[[Integer, Integer]] points) -> Integer?
11
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: placement_machines
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.4
5
+ platform: arm64-darwin
6
+ authors:
7
+ - Abdelkader Boudih
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-06 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A thin binding to the no_std placement-machines Rust core. Provides deterministic
14
+ rendezvous scores, ring ownership and distinct-domain replica choices without membership
15
+ services.
16
+ email:
17
+ - oss@seuros.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - CHANGELOG.md
23
+ - LICENSE.txt
24
+ - README.md
25
+ - ext/placement_machines_native/core/CHANGELOG.md
26
+ - ext/placement_machines_native/core/LICENSE
27
+ - ext/placement_machines_native/core/README.md
28
+ - ext/placement_machines_native/extconf.rb
29
+ - lib/placement_machines.rb
30
+ - lib/placement_machines/version.rb
31
+ - lib/placement_machines_native/placement_machines_native.bundle
32
+ - sig/placement_machines.rbs
33
+ homepage: https://github.com/seuros/placement_machines
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ source_code_uri: https://github.com/seuros/placement_machines
38
+ bug_tracker_uri: https://github.com/seuros/placement_machines/issues
39
+ rubygems_mfa_required: 'true'
40
+ cargo_crate_name: placement_machines_native
41
+ cargo_manifest_path: ext/placement_machines_native/ffi/Cargo.toml
42
+ post_install_message:
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: '4.0'
51
+ - - "<"
52
+ - !ruby/object:Gem::Version
53
+ version: 4.1.dev
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.5.23
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Native rendezvous and ring placement decisions for Ruby
64
+ test_files: []