hungry-ghost-hive 0.43.0 → 0.43.2
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.
- package/dist/cli/commands/agents.d.ts.map +1 -1
- package/dist/cli/commands/agents.js +4 -11
- package/dist/cli/commands/agents.js.map +1 -1
- package/dist/cli/commands/approach.d.ts.map +1 -1
- package/dist/cli/commands/approach.js +2 -6
- package/dist/cli/commands/approach.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +9 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +3 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/manager/index.d.ts +2 -27
- package/dist/cli/commands/manager/index.d.ts.map +1 -1
- package/dist/cli/commands/manager/index.js +23 -1519
- package/dist/cli/commands/manager/index.js.map +1 -1
- package/dist/cli/commands/manager/manager-utils.d.ts +9 -0
- package/dist/cli/commands/manager/manager-utils.d.ts.map +1 -0
- package/dist/cli/commands/manager/manager-utils.js +49 -0
- package/dist/cli/commands/manager/manager-utils.js.map +1 -0
- package/dist/cli/commands/manager/pr-sync-orchestrator.d.ts +7 -0
- package/dist/cli/commands/manager/pr-sync-orchestrator.d.ts.map +1 -0
- package/dist/cli/commands/manager/pr-sync-orchestrator.js +537 -0
- package/dist/cli/commands/manager/pr-sync-orchestrator.js.map +1 -0
- package/dist/cli/commands/manager/qa-review-handler.d.ts +15 -0
- package/dist/cli/commands/manager/qa-review-handler.d.ts.map +1 -0
- package/dist/cli/commands/manager/qa-review-handler.js +290 -0
- package/dist/cli/commands/manager/qa-review-handler.js.map +1 -0
- package/dist/cli/commands/manager/stuck-story-helpers.d.ts +32 -0
- package/dist/cli/commands/manager/stuck-story-helpers.d.ts.map +1 -0
- package/dist/cli/commands/manager/stuck-story-helpers.js +163 -0
- package/dist/cli/commands/manager/stuck-story-helpers.js.map +1 -0
- package/dist/cli/commands/manager/stuck-story-processor.d.ts +8 -0
- package/dist/cli/commands/manager/stuck-story-processor.d.ts.map +1 -0
- package/dist/cli/commands/manager/stuck-story-processor.js +392 -0
- package/dist/cli/commands/manager/stuck-story-processor.js.map +1 -0
- package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts +3 -0
- package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -0
- package/dist/cli/commands/manager/tech-lead-lifecycle.js +141 -0
- package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -0
- package/dist/cli/commands/my-stories.d.ts.map +1 -1
- package/dist/cli/commands/my-stories.js +5 -20
- package/dist/cli/commands/my-stories.js.map +1 -1
- package/dist/cli/commands/pr.js +7 -22
- package/dist/cli/commands/pr.js.map +1 -1
- package/dist/cli/commands/progress.d.ts.map +1 -1
- package/dist/cli/commands/progress.js +2 -5
- package/dist/cli/commands/progress.js.map +1 -1
- package/dist/cli/commands/resume.d.ts.map +1 -1
- package/dist/cli/commands/resume.js +3 -6
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +2 -5
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/stories.d.ts.map +1 -1
- package/dist/cli/commands/stories.js +2 -5
- package/dist/cli/commands/stories.js.map +1 -1
- package/dist/cluster/adapters.d.ts +3 -2
- package/dist/cluster/adapters.d.ts.map +1 -1
- package/dist/cluster/adapters.js +2 -11
- package/dist/cluster/adapters.js.map +1 -1
- package/dist/cluster/cluster-http-server.d.ts +20 -0
- package/dist/cluster/cluster-http-server.d.ts.map +1 -0
- package/dist/cluster/cluster-http-server.js +140 -0
- package/dist/cluster/cluster-http-server.js.map +1 -0
- package/dist/cluster/heartbeat-manager.d.ts +24 -0
- package/dist/cluster/heartbeat-manager.d.ts.map +1 -0
- package/dist/cluster/heartbeat-manager.js +74 -0
- package/dist/cluster/heartbeat-manager.js.map +1 -0
- package/dist/cluster/raft-state-machine.d.ts +48 -0
- package/dist/cluster/raft-state-machine.d.ts.map +1 -0
- package/dist/cluster/raft-state-machine.js +207 -0
- package/dist/cluster/raft-state-machine.js.map +1 -0
- package/dist/cluster/runtime.d.ts +5 -29
- package/dist/cluster/runtime.d.ts.map +1 -1
- package/dist/cluster/runtime.js +58 -406
- package/dist/cluster/runtime.js.map +1 -1
- package/dist/integrations/jira/sync.d.ts +2 -5
- package/dist/integrations/jira/sync.d.ts.map +1 -1
- package/dist/integrations/jira/sync.js +116 -178
- package/dist/integrations/jira/sync.js.map +1 -1
- package/dist/utils/cli-helpers.d.ts +19 -0
- package/dist/utils/cli-helpers.d.ts.map +1 -0
- package/dist/utils/cli-helpers.js +51 -0
- package/dist/utils/cli-helpers.js.map +1 -0
- package/dist/utils/cli-helpers.test.d.ts +2 -0
- package/dist/utils/cli-helpers.test.d.ts.map +1 -0
- package/dist/utils/cli-helpers.test.js +100 -0
- package/dist/utils/cli-helpers.test.js.map +1 -0
- package/dist/utils/github-cli.d.ts +3 -0
- package/dist/utils/github-cli.d.ts.map +1 -0
- package/dist/utils/github-cli.js +4 -0
- package/dist/utils/github-cli.js.map +1 -0
- package/dist/utils/pr-sync.d.ts.map +1 -1
- package/dist/utils/pr-sync.js +1 -2
- package/dist/utils/pr-sync.js.map +1 -1
- package/dist/utils/story-status.d.ts +19 -0
- package/dist/utils/story-status.d.ts.map +1 -0
- package/dist/utils/story-status.js +58 -0
- package/dist/utils/story-status.js.map +1 -0
- package/dist/utils/story-status.test.d.ts +2 -0
- package/dist/utils/story-status.test.d.ts.map +1 -0
- package/dist/utils/story-status.test.js +65 -0
- package/dist/utils/story-status.test.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/agents.ts +3 -11
- package/src/cli/commands/approach.ts +2 -7
- package/src/cli/commands/init.test.ts +4 -0
- package/src/cli/commands/init.ts +9 -0
- package/src/cli/commands/manager/index.ts +166 -2236
- package/src/cli/commands/manager/manager-utils.ts +85 -0
- package/src/cli/commands/manager/pr-sync-orchestrator.ts +659 -0
- package/src/cli/commands/manager/qa-review-handler.ts +399 -0
- package/src/cli/commands/manager/stuck-story-helpers.ts +255 -0
- package/src/cli/commands/manager/stuck-story-processor.ts +604 -0
- package/src/cli/commands/manager/tech-lead-lifecycle.ts +210 -0
- package/src/cli/commands/my-stories.ts +5 -30
- package/src/cli/commands/pr.ts +6 -22
- package/src/cli/commands/progress.ts +2 -7
- package/src/cli/commands/resume.ts +3 -6
- package/src/cli/commands/status.ts +2 -5
- package/src/cli/commands/stories.ts +2 -5
- package/src/cluster/adapters.ts +3 -12
- package/src/cluster/cluster-http-server.ts +187 -0
- package/src/cluster/heartbeat-manager.ts +112 -0
- package/src/cluster/raft-state-machine.ts +267 -0
- package/src/cluster/runtime.ts +71 -515
- package/src/integrations/jira/sync.ts +157 -215
- package/src/utils/cli-helpers.test.ts +138 -0
- package/src/utils/cli-helpers.ts +61 -0
- package/src/utils/github-cli.ts +4 -0
- package/src/utils/pr-sync.ts +1 -3
- package/src/utils/story-status.test.ts +74 -0
- package/src/utils/story-status.ts +62 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Licensed under the Hungry Ghost Hive License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
import type { ClusterConfig, ClusterPeerConfig } from '../config/schema.js';
|
|
4
|
+
import type { RaftStateMachine } from './raft-state-machine.js';
|
|
5
|
+
|
|
6
|
+
interface HeartbeatRequest {
|
|
7
|
+
term: number;
|
|
8
|
+
leader_id: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface HeartbeatResponse {
|
|
12
|
+
term: number;
|
|
13
|
+
success: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface HeartbeatManagerDeps {
|
|
17
|
+
raft: RaftStateMachine;
|
|
18
|
+
postJson: <T>(peer: ClusterPeerConfig, path: string, body: unknown) => Promise<T | null>;
|
|
19
|
+
isActive: () => boolean;
|
|
20
|
+
handleBackgroundError: (error: unknown) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class HeartbeatManager {
|
|
24
|
+
private heartbeatTimer: NodeJS.Timeout | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly config: ClusterConfig,
|
|
28
|
+
private readonly deps: HeartbeatManagerDeps
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
startHeartbeatLoop(): void {
|
|
32
|
+
this.heartbeatTimer = setInterval(() => {
|
|
33
|
+
if (!this.config.enabled) return;
|
|
34
|
+
if (this.deps.raft.role !== 'leader') return;
|
|
35
|
+
void this.sendHeartbeats().catch(error => this.deps.handleBackgroundError(error));
|
|
36
|
+
}, this.config.heartbeat_interval_ms);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
stopHeartbeatLoop(): void {
|
|
40
|
+
if (this.heartbeatTimer) {
|
|
41
|
+
clearInterval(this.heartbeatTimer);
|
|
42
|
+
this.heartbeatTimer = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async sendHeartbeats(): Promise<void> {
|
|
47
|
+
if (!this.deps.isActive()) return;
|
|
48
|
+
|
|
49
|
+
const { raft } = this.deps;
|
|
50
|
+
|
|
51
|
+
const heartbeat: HeartbeatRequest = {
|
|
52
|
+
term: raft.currentTerm,
|
|
53
|
+
leader_id: this.config.node_id,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
raft.appendDurableEntry('heartbeat_sent', {
|
|
57
|
+
term: raft.currentTerm,
|
|
58
|
+
leader_id: this.config.node_id,
|
|
59
|
+
peer_count: this.config.peers.filter(peer => peer.id !== this.config.node_id).length,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await Promise.all(
|
|
63
|
+
this.config.peers
|
|
64
|
+
.filter(peer => peer.id !== this.config.node_id)
|
|
65
|
+
.map(async peer => {
|
|
66
|
+
const response = await this.deps.postJson<HeartbeatResponse>(
|
|
67
|
+
peer,
|
|
68
|
+
'/cluster/v1/election/heartbeat',
|
|
69
|
+
heartbeat
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (response && response.term > raft.currentTerm) {
|
|
73
|
+
raft.stepDown(response.term, peer.id);
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
handleHeartbeat(body: unknown): HeartbeatResponse {
|
|
80
|
+
const { raft } = this.deps;
|
|
81
|
+
|
|
82
|
+
const request = body as Partial<HeartbeatRequest>;
|
|
83
|
+
const term = Number(request.term || 0);
|
|
84
|
+
const leaderId = typeof request.leader_id === 'string' ? request.leader_id : null;
|
|
85
|
+
|
|
86
|
+
if (term < raft.currentTerm) {
|
|
87
|
+
return { term: raft.currentTerm, success: false };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const changed =
|
|
91
|
+
term > raft.currentTerm || leaderId !== raft.leaderId || raft.role !== 'follower';
|
|
92
|
+
|
|
93
|
+
if (term > raft.currentTerm) {
|
|
94
|
+
raft.stepDown(term, leaderId);
|
|
95
|
+
} else {
|
|
96
|
+
raft.role = 'follower';
|
|
97
|
+
raft.leaderId = leaderId;
|
|
98
|
+
raft.persistRaftState();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
raft.resetElectionDeadline();
|
|
102
|
+
|
|
103
|
+
if (changed) {
|
|
104
|
+
raft.appendDurableEntry('heartbeat_received', {
|
|
105
|
+
term,
|
|
106
|
+
leader_id: leaderId,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { term: raft.currentTerm, success: true };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Licensed under the Hungry Ghost Hive License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import type { ClusterConfig, ClusterPeerConfig } from '../config/schema.js';
|
|
5
|
+
import type { DurableLogEntryType } from './raft-store.js';
|
|
6
|
+
import { RaftMetadataStore } from './raft-store.js';
|
|
7
|
+
|
|
8
|
+
type NodeRole = 'leader' | 'follower' | 'candidate';
|
|
9
|
+
|
|
10
|
+
interface VoteRequest {
|
|
11
|
+
term: number;
|
|
12
|
+
candidate_id: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface VoteResponse {
|
|
16
|
+
term: number;
|
|
17
|
+
vote_granted: boolean;
|
|
18
|
+
leader_id: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RaftStateMachineDeps {
|
|
22
|
+
postJson: <T>(peer: ClusterPeerConfig, path: string, body: unknown) => Promise<T | null>;
|
|
23
|
+
isActive: () => boolean;
|
|
24
|
+
handleBackgroundError: (error: unknown) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class RaftStateMachine {
|
|
28
|
+
role: NodeRole = 'follower';
|
|
29
|
+
currentTerm = 0;
|
|
30
|
+
votedFor: string | null = null;
|
|
31
|
+
leaderId: string | null = null;
|
|
32
|
+
|
|
33
|
+
private electionDeadline = 0;
|
|
34
|
+
private electionInFlight = false;
|
|
35
|
+
private electionTimer: NodeJS.Timeout | null = null;
|
|
36
|
+
private raftStore: RaftMetadataStore | null = null;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly config: ClusterConfig,
|
|
40
|
+
private readonly deps: RaftStateMachineDeps
|
|
41
|
+
) {}
|
|
42
|
+
|
|
43
|
+
initializeRaftStore(hiveDir: string): void {
|
|
44
|
+
if (this.raftStore) return;
|
|
45
|
+
|
|
46
|
+
const clusterDir = join(hiveDir, 'cluster');
|
|
47
|
+
this.raftStore = new RaftMetadataStore({
|
|
48
|
+
clusterDir,
|
|
49
|
+
nodeId: this.config.node_id,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const persisted = this.raftStore.getState();
|
|
53
|
+
this.currentTerm = persisted.current_term;
|
|
54
|
+
this.votedFor = persisted.voted_for;
|
|
55
|
+
this.leaderId = persisted.leader_id;
|
|
56
|
+
this.role = 'follower';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getRaftStore(): RaftMetadataStore | null {
|
|
60
|
+
return this.raftStore;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clearRaftStore(): void {
|
|
64
|
+
this.raftStore = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
startElectionLoop(): void {
|
|
68
|
+
this.resetElectionDeadline();
|
|
69
|
+
this.electionTimer = setInterval(() => {
|
|
70
|
+
if (!this.config.enabled) return;
|
|
71
|
+
if (this.role === 'leader') return;
|
|
72
|
+
|
|
73
|
+
if (Date.now() >= this.electionDeadline) {
|
|
74
|
+
void this.startElection().catch(error => this.deps.handleBackgroundError(error));
|
|
75
|
+
}
|
|
76
|
+
}, 250);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stopElectionLoop(): void {
|
|
80
|
+
if (this.electionTimer) {
|
|
81
|
+
clearInterval(this.electionTimer);
|
|
82
|
+
this.electionTimer = null;
|
|
83
|
+
}
|
|
84
|
+
this.electionInFlight = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async startElection(): Promise<void> {
|
|
88
|
+
if (!this.config.enabled || this.electionInFlight || !this.deps.isActive()) return;
|
|
89
|
+
|
|
90
|
+
this.electionInFlight = true;
|
|
91
|
+
const electionTerm = this.currentTerm + 1;
|
|
92
|
+
|
|
93
|
+
this.currentTerm = electionTerm;
|
|
94
|
+
this.role = 'candidate';
|
|
95
|
+
this.votedFor = this.config.node_id;
|
|
96
|
+
this.leaderId = null;
|
|
97
|
+
this.resetElectionDeadline();
|
|
98
|
+
this.persistRaftState();
|
|
99
|
+
this.appendDurableEntry('election_start', {
|
|
100
|
+
term: electionTerm,
|
|
101
|
+
candidate_id: this.config.node_id,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let votes = 1;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await Promise.all(
|
|
108
|
+
this.config.peers
|
|
109
|
+
.filter(peer => peer.id !== this.config.node_id)
|
|
110
|
+
.map(async peer => {
|
|
111
|
+
const response = await this.deps.postJson<VoteResponse>(
|
|
112
|
+
peer,
|
|
113
|
+
'/cluster/v1/election/request-vote',
|
|
114
|
+
{
|
|
115
|
+
term: electionTerm,
|
|
116
|
+
candidate_id: this.config.node_id,
|
|
117
|
+
} satisfies VoteRequest
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (!response) return;
|
|
121
|
+
|
|
122
|
+
if (response.term > this.currentTerm) {
|
|
123
|
+
this.stepDown(response.term, response.leader_id);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
this.role === 'candidate' &&
|
|
129
|
+
this.currentTerm === electionTerm &&
|
|
130
|
+
response.vote_granted
|
|
131
|
+
) {
|
|
132
|
+
votes += 1;
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
this.role === 'candidate' &&
|
|
139
|
+
this.currentTerm === electionTerm &&
|
|
140
|
+
votes >= this.quorum()
|
|
141
|
+
) {
|
|
142
|
+
this.role = 'leader';
|
|
143
|
+
this.leaderId = this.config.node_id;
|
|
144
|
+
this.votedFor = null;
|
|
145
|
+
this.persistRaftState();
|
|
146
|
+
this.appendDurableEntry('election_won', {
|
|
147
|
+
term: electionTerm,
|
|
148
|
+
votes,
|
|
149
|
+
quorum: this.quorum(),
|
|
150
|
+
leader_id: this.config.node_id,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
} finally {
|
|
154
|
+
this.electionInFlight = false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
handleVoteRequest(body: unknown): VoteResponse {
|
|
159
|
+
const request = body as Partial<VoteRequest>;
|
|
160
|
+
const term = Number(request.term || 0);
|
|
161
|
+
const candidateId = typeof request.candidate_id === 'string' ? request.candidate_id : '';
|
|
162
|
+
|
|
163
|
+
if (!candidateId) {
|
|
164
|
+
return { term: this.currentTerm, vote_granted: false, leader_id: this.leaderId };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (term < this.currentTerm) {
|
|
168
|
+
return { term: this.currentTerm, vote_granted: false, leader_id: this.leaderId };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (term > this.currentTerm) {
|
|
172
|
+
this.stepDown(term, null);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const canVote = this.votedFor === null || this.votedFor === candidateId;
|
|
176
|
+
if (canVote) {
|
|
177
|
+
this.votedFor = candidateId;
|
|
178
|
+
this.resetElectionDeadline();
|
|
179
|
+
this.persistRaftState();
|
|
180
|
+
this.appendDurableEntry('vote_granted', {
|
|
181
|
+
term: this.currentTerm,
|
|
182
|
+
candidate_id: candidateId,
|
|
183
|
+
});
|
|
184
|
+
return { term: this.currentTerm, vote_granted: true, leader_id: this.leaderId };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { term: this.currentTerm, vote_granted: false, leader_id: this.leaderId };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
stepDown(term: number, leaderId: string | null): void {
|
|
191
|
+
const previousRole = this.role;
|
|
192
|
+
const previousTerm = this.currentTerm;
|
|
193
|
+
|
|
194
|
+
this.currentTerm = term;
|
|
195
|
+
this.role = 'follower';
|
|
196
|
+
this.votedFor = null;
|
|
197
|
+
this.leaderId = leaderId;
|
|
198
|
+
this.resetElectionDeadline();
|
|
199
|
+
this.persistRaftState();
|
|
200
|
+
|
|
201
|
+
this.appendDurableEntry('state_transition', {
|
|
202
|
+
previous_role: previousRole,
|
|
203
|
+
previous_term: previousTerm,
|
|
204
|
+
current_term: this.currentTerm,
|
|
205
|
+
leader_id: leaderId,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
quorum(): number {
|
|
210
|
+
const nodes = this.config.peers.length + 1;
|
|
211
|
+
return Math.floor(nodes / 2) + 1;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
resetElectionDeadline(): void {
|
|
215
|
+
const min = this.config.election_timeout_min_ms;
|
|
216
|
+
const max = Math.max(min, this.config.election_timeout_max_ms);
|
|
217
|
+
const spread = max - min;
|
|
218
|
+
const jitter = spread === 0 ? 0 : Math.floor(Math.random() * spread);
|
|
219
|
+
this.electionDeadline = Date.now() + min + jitter;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
persistRaftState(): void {
|
|
223
|
+
if (!this.raftStore) return;
|
|
224
|
+
|
|
225
|
+
this.raftStore.setState({
|
|
226
|
+
current_term: this.currentTerm,
|
|
227
|
+
voted_for: this.votedFor,
|
|
228
|
+
leader_id: this.leaderId,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
appendDurableEntry(type: DurableLogEntryType, metadata: Record<string, unknown>): void {
|
|
233
|
+
if (!this.raftStore) return;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
this.raftStore.appendEntry({ type, term: this.currentTerm, metadata });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const err = error as NodeJS.ErrnoException;
|
|
239
|
+
if (err.code === 'ENOENT') return;
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
appendClusterEventsToDurableLog(events: import('./replication.js').ClusterEvent[]): number {
|
|
245
|
+
if (!this.raftStore) return 0;
|
|
246
|
+
|
|
247
|
+
const appended = this.raftStore.appendClusterEvents(events, this.currentTerm);
|
|
248
|
+
this.persistRaftState();
|
|
249
|
+
return appended;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
getRaftStoreState(): {
|
|
253
|
+
commit_index: number;
|
|
254
|
+
last_applied: number;
|
|
255
|
+
last_log_index: number;
|
|
256
|
+
} | null {
|
|
257
|
+
return this.raftStore?.getState() ?? null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getLeaderUrl(): string | null {
|
|
261
|
+
if (!this.leaderId) return null;
|
|
262
|
+
if (this.leaderId === this.config.node_id) return this.config.public_url;
|
|
263
|
+
|
|
264
|
+
const peer = this.config.peers.find(item => item.id === this.leaderId);
|
|
265
|
+
return peer?.url || null;
|
|
266
|
+
}
|
|
267
|
+
}
|