Loading src/cluster.h +14 −0 Changes for src/cluster.h: 14 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -128,6 +128,20 @@ namespace fenrirds { bool isRunning() const { return running_; } // RID partitioning: this node's index among the peers and the peer // count, so each participant allocates RIDs from a disjoint residue // class (rid ≡ self mod count) instead of a shared counter — no // coordination, no duplicate SIDs across the cluster. Returns false // when this node cannot identify itself in PEERS (caller falls back to // the single-node scheme). bool ridPartition(uint32_t &self, uint32_t &count) const { if (self_idx_ == SIZE_MAX || cfg_.peers.empty()) return false; self = static_cast<uint32_t>(self_idx_); count = static_cast<uint32_t>(cfg_.peers.size()); return self < count; } // Block until at least k nodes are reachable (up to timeout_s seconds). // Returns number of nodes online. size_t waitForPeers(int timeout_s = 10); Loading src/dsdb/directory.cpp +54 −18 Changes for src/dsdb/directory.cpp: 54 added lines, 18 removed lines. Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ #include "directory.h" #include "schema.h" #include "replication.h" #include "cluster.h" namespace fenrirds { namespace dsdb { Loading Loading @@ -181,31 +182,66 @@ Secrets Directory::secretsOf(const uuid::uuid &guid) { return s; } void Directory::initRidPartition(uint32_t k, uint32_t n) { /* seed _ridM so the next RID this node hands out (RID_FIRST_FREE + k + m*n) * is strictly above every RID it already owns. The high-water comes from a * scan of existing objects — not a shared counter — so no cross-node * coordination is needed and a lost counter write can never cause reuse. */ _ridK = k; _ridN = n ? n : 1; uint32_t floor = RID_FIRST_FREE; if (Entry *head = findByDnLocked(_Meta.baseDn)) floor = uint32_t(intValue(head->attrs, "@nextRid", RID_FIRST_FREE)); bool found = false; uint32_t maxM = 0; for (auto &e : _Entries) { if (e.objectSid.empty() || !e.objectSid.inDomain(_Meta.sid)) continue; uint32_t r = e.objectSid.rid(); if (r < RID_FIRST_FREE) continue; /* well-known RID */ if (((r - RID_FIRST_FREE) % _ridN) != (_ridK % _ridN)) continue; /* not our class */ uint32_t m = (r - RID_FIRST_FREE - _ridK) / _ridN; if (!found || m > maxM) { maxM = m; found = true; } } _ridM = found ? maxM + 1 : 0; /* respect any pre-seeded @nextRid floor (e.g. from provisioning) */ while ((RID_FIRST_FREE + _ridK + _ridM * _ridN) < floor) ++_ridM; _ridInit = true; } uint32_t Directory::allocateRid() { /* the RID pool lives on the domain head (@nextRid). Grows one at a time; * a proper RID manager hands out pools, this is enough for one DC. * * RID safety: only a single authoritative writer may allocate, or two * nodes hand out the same RID (→ two principals with the same SID). In * master/slave replication the master is that single writer; a slave is a * read-only replica, so refuse here — otherwise a write on a slave would * burn a RID locally that the next snapshot from the master overwrites. * (True multi-master still needs per-node RID pools, like AD's RID master.) */ /* RID safety. Each participant hands out RIDs from a DISJOINT residue * class: rid = RID_FIRST_FREE + k + m*n, where (k, n) is this node's index * and the peer count. Different nodes have different k → their RID sets * never intersect, so no two nodes ever mint the same SID — no shared * counter, no coordination, safe even on the eventually-consistent cluster * store. Single node / master-slave is simply n=1, k=0 (the classic * contiguous scheme). A replication slave is read-only and must not * allocate at all. */ if (g_Replication && g_Replication->isSlave()) throw std::runtime_error("RID allocation refused: this node is a replication slave (read-only); make changes on the master"); std::lock_guard<std::mutex> lk(_Mtx); ensureFresh(); Entry *head = findByDnLocked(_Meta.baseDn); if (!head) if (!findByDnLocked(_Meta.baseDn)) throw std::runtime_error("domain not provisioned"); uint32_t next = uint32_t(intValue(head->attrs, "@nextRid", RID_FIRST_FREE)); AttrMap delta = head->attrs; setInt(delta, "@nextRid", next + 1); storeObject(_Backend, head->objectGuid, head->dn, delta, objectSecrets(_Backend, head->objectGuid), false); head->attrs = delta; _Revision = _Backend.getRevesion(); return next; uint32_t k = 0, n = 1; if (g_Cluster && g_Cluster->isRunning()) { uint32_t s = 0, c = 0; if (g_Cluster->ridPartition(s, c) && c > 0) { k = s; n = c; } } /* (re)seed when first used or the cluster topology changed */ if (!_ridInit || k != _ridK || n != _ridN) initRidPartition(k, n); uint32_t rid = RID_FIRST_FREE + _ridK + _ridM * _ridN; ++_ridM; return rid; } uuid::uuid Directory::create(const Dn &dn, AttrMap attrs, const Secrets *secrets) { Loading src/dsdb/directory.h +12 −0 Changes for src/dsdb/directory.h: 12 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -129,6 +129,9 @@ namespace fenrirds { void loadAll(); /* (re)build the entry cache */ void addEntryToIndex(const Entry &e, size_t idx); Entry *findByDnLocked(const Dn &dn); /* seed the per-node RID counter (_ridM) for residue class k of n * from the highest existing RID this node already owns */ void initRidPartition(uint32_t k, uint32_t n); AuthBackend &_Backend; DomainMeta _Meta; Loading @@ -136,6 +139,15 @@ namespace fenrirds { std::mutex _Mtx; size_t _Revision = (size_t)-1; std::vector<Entry> _Entries; /* RID allocation (see allocateRid): each node hands out RIDs from a * disjoint residue class rid = RID_FIRST_FREE + k + m*n, so cluster * participants never collide. k/n are this node's partition, m the * local counter, seeded by a scan of existing objects. */ uint32_t _ridK = 0; uint32_t _ridN = 1; uint32_t _ridM = 0; bool _ridInit = false; std::map<std::string, size_t> _ByDnKey; /* dn.key() -> index */ std::map<std::string, size_t> _BySid; /* sid.binary() -> index */ }; Loading Loading
src/cluster.h +14 −0 Changes for src/cluster.h: 14 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -128,6 +128,20 @@ namespace fenrirds { bool isRunning() const { return running_; } // RID partitioning: this node's index among the peers and the peer // count, so each participant allocates RIDs from a disjoint residue // class (rid ≡ self mod count) instead of a shared counter — no // coordination, no duplicate SIDs across the cluster. Returns false // when this node cannot identify itself in PEERS (caller falls back to // the single-node scheme). bool ridPartition(uint32_t &self, uint32_t &count) const { if (self_idx_ == SIZE_MAX || cfg_.peers.empty()) return false; self = static_cast<uint32_t>(self_idx_); count = static_cast<uint32_t>(cfg_.peers.size()); return self < count; } // Block until at least k nodes are reachable (up to timeout_s seconds). // Returns number of nodes online. size_t waitForPeers(int timeout_s = 10); Loading
src/dsdb/directory.cpp +54 −18 Changes for src/dsdb/directory.cpp: 54 added lines, 18 removed lines. Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ #include "directory.h" #include "schema.h" #include "replication.h" #include "cluster.h" namespace fenrirds { namespace dsdb { Loading Loading @@ -181,31 +182,66 @@ Secrets Directory::secretsOf(const uuid::uuid &guid) { return s; } void Directory::initRidPartition(uint32_t k, uint32_t n) { /* seed _ridM so the next RID this node hands out (RID_FIRST_FREE + k + m*n) * is strictly above every RID it already owns. The high-water comes from a * scan of existing objects — not a shared counter — so no cross-node * coordination is needed and a lost counter write can never cause reuse. */ _ridK = k; _ridN = n ? n : 1; uint32_t floor = RID_FIRST_FREE; if (Entry *head = findByDnLocked(_Meta.baseDn)) floor = uint32_t(intValue(head->attrs, "@nextRid", RID_FIRST_FREE)); bool found = false; uint32_t maxM = 0; for (auto &e : _Entries) { if (e.objectSid.empty() || !e.objectSid.inDomain(_Meta.sid)) continue; uint32_t r = e.objectSid.rid(); if (r < RID_FIRST_FREE) continue; /* well-known RID */ if (((r - RID_FIRST_FREE) % _ridN) != (_ridK % _ridN)) continue; /* not our class */ uint32_t m = (r - RID_FIRST_FREE - _ridK) / _ridN; if (!found || m > maxM) { maxM = m; found = true; } } _ridM = found ? maxM + 1 : 0; /* respect any pre-seeded @nextRid floor (e.g. from provisioning) */ while ((RID_FIRST_FREE + _ridK + _ridM * _ridN) < floor) ++_ridM; _ridInit = true; } uint32_t Directory::allocateRid() { /* the RID pool lives on the domain head (@nextRid). Grows one at a time; * a proper RID manager hands out pools, this is enough for one DC. * * RID safety: only a single authoritative writer may allocate, or two * nodes hand out the same RID (→ two principals with the same SID). In * master/slave replication the master is that single writer; a slave is a * read-only replica, so refuse here — otherwise a write on a slave would * burn a RID locally that the next snapshot from the master overwrites. * (True multi-master still needs per-node RID pools, like AD's RID master.) */ /* RID safety. Each participant hands out RIDs from a DISJOINT residue * class: rid = RID_FIRST_FREE + k + m*n, where (k, n) is this node's index * and the peer count. Different nodes have different k → their RID sets * never intersect, so no two nodes ever mint the same SID — no shared * counter, no coordination, safe even on the eventually-consistent cluster * store. Single node / master-slave is simply n=1, k=0 (the classic * contiguous scheme). A replication slave is read-only and must not * allocate at all. */ if (g_Replication && g_Replication->isSlave()) throw std::runtime_error("RID allocation refused: this node is a replication slave (read-only); make changes on the master"); std::lock_guard<std::mutex> lk(_Mtx); ensureFresh(); Entry *head = findByDnLocked(_Meta.baseDn); if (!head) if (!findByDnLocked(_Meta.baseDn)) throw std::runtime_error("domain not provisioned"); uint32_t next = uint32_t(intValue(head->attrs, "@nextRid", RID_FIRST_FREE)); AttrMap delta = head->attrs; setInt(delta, "@nextRid", next + 1); storeObject(_Backend, head->objectGuid, head->dn, delta, objectSecrets(_Backend, head->objectGuid), false); head->attrs = delta; _Revision = _Backend.getRevesion(); return next; uint32_t k = 0, n = 1; if (g_Cluster && g_Cluster->isRunning()) { uint32_t s = 0, c = 0; if (g_Cluster->ridPartition(s, c) && c > 0) { k = s; n = c; } } /* (re)seed when first used or the cluster topology changed */ if (!_ridInit || k != _ridK || n != _ridN) initRidPartition(k, n); uint32_t rid = RID_FIRST_FREE + _ridK + _ridM * _ridN; ++_ridM; return rid; } uuid::uuid Directory::create(const Dn &dn, AttrMap attrs, const Secrets *secrets) { Loading
src/dsdb/directory.h +12 −0 Changes for src/dsdb/directory.h: 12 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -129,6 +129,9 @@ namespace fenrirds { void loadAll(); /* (re)build the entry cache */ void addEntryToIndex(const Entry &e, size_t idx); Entry *findByDnLocked(const Dn &dn); /* seed the per-node RID counter (_ridM) for residue class k of n * from the highest existing RID this node already owns */ void initRidPartition(uint32_t k, uint32_t n); AuthBackend &_Backend; DomainMeta _Meta; Loading @@ -136,6 +139,15 @@ namespace fenrirds { std::mutex _Mtx; size_t _Revision = (size_t)-1; std::vector<Entry> _Entries; /* RID allocation (see allocateRid): each node hands out RIDs from a * disjoint residue class rid = RID_FIRST_FREE + k + m*n, so cluster * participants never collide. k/n are this node's partition, m the * local counter, seeded by a scan of existing objects. */ uint32_t _ridK = 0; uint32_t _ridN = 1; uint32_t _ridM = 0; bool _ridInit = false; std::map<std::string, size_t> _ByDnKey; /* dn.key() -> index */ std::map<std::string, size_t> _BySid; /* sid.binary() -> index */ }; Loading