Skip to content

Commit aad7c1d

Browse files
committed
Release v1.0.7
1 parent c5e2f25 commit aad7c1d

File tree

12 files changed

+228
-115
lines changed

12 files changed

+228
-115
lines changed

Diff for: .github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
password: ${{ secrets.DOCKER_PASSWORD }}
1717
- uses: stepchowfun/toast/.github/actions/toast@main
1818
with:
19-
tasks: build test lint release run
19+
tasks: build test lint release validate_release run
2020
docker_repo: stephanmisc/toast
2121
write_remote_cache: ${{ github.event_name == 'push' }}
2222
- run: |

Diff for: CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.0.7] - 2023-07-01
9+
10+
### Changed
11+
- Even if there's no value to propose, the proposer is run periodically to learn if a value was chosen and let the other nodes know about it.
12+
813
## [1.0.6] - 2023-07-01
914

1015
### Changed

Diff for: Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "paxos"
3-
version = "1.0.6"
3+
version = "1.0.7"
44
authors = ["Stephan Boyer <[email protected]>"]
55
edition = "2021"
66
description = "An implementation of single-decree Paxos."

Diff for: integration-tests/test-0.sh

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Start two of the three Paxos instances in the background.
5+
echo 'Starting Paxos instance 0…'
6+
LOG_LEVEL=debug "$PAXOS" --node 0 --propose foo | tee node-0.txt &
7+
echo 'Starting Paxos instance 1…'
8+
LOG_LEVEL=debug "$PAXOS" --node 1 | tee node-1.txt &
9+
10+
# Wait for the two nodes to achieve consensus.
11+
echo 'Waiting for Paxos instance 0…'
12+
grep -q 'foo' <(tail -F node-0.txt)
13+
echo 'Waiting for Paxos instance 1…'
14+
grep -q 'foo' <(tail -F node-1.txt)
15+
16+
# Start the third node.
17+
echo 'Starting Paxos instance 2…'
18+
LOG_LEVEL=debug "$PAXOS" --node 2 --propose bar | tee node-2.txt &
19+
20+
# Wait for the third node to learn the chosen value.
21+
echo 'Waiting for Paxos instance 2…'
22+
grep -q 'foo' <(tail -F node-2.txt)
23+
24+
# Kill all the subprocesses spawned by this script.
25+
pkill -P "$$"
26+
27+
# Clean up the files.
28+
rm node-0.txt node-1.txt node-2.txt

Diff for: integration-tests/test-1.sh

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Start the Paxos instances in the background.
5+
echo 'Starting Paxos instance 0…'
6+
LOG_LEVEL=debug "$PAXOS" --node 0 --propose foo | tee node-0.txt &
7+
echo 'Starting Paxos instance 1…'
8+
LOG_LEVEL=debug "$PAXOS" --node 1 --propose bar | tee node-1.txt &
9+
echo 'Starting Paxos instance 2…'
10+
LOG_LEVEL=debug "$PAXOS" --node 2 --propose baz | tee node-2.txt &
11+
12+
# Wait for the nodes to achieve consensus.
13+
echo 'Waiting for Paxos instance 0…'
14+
grep -q 'foo\|bar\|baz' <(tail -F node-0.txt)
15+
echo 'Waiting for Paxos instance 1…'
16+
grep -q 'foo\|bar\|baz' <(tail -F node-1.txt)
17+
echo 'Waiting for Paxos instance 2…'
18+
grep -q 'foo\|bar\|baz' <(tail -F node-2.txt)
19+
20+
# Kill all the subprocesses spawned by this script.
21+
pkill -P "$$"
22+
23+
# Clean up the files.
24+
rm node-0.txt node-1.txt node-2.txt

Diff for: src/acceptor.rs

+66-50
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use {
2-
crate::state::{ProposalNumber, State},
2+
crate::state::{self, ProposalNumber},
33
hyper::{
44
header::CONTENT_TYPE,
55
server::conn::AddrStream,
@@ -29,7 +29,7 @@ pub const CHOOSE_ENDPOINT: &str = "/choose";
2929
#[derive(Clone, Deserialize, Serialize)]
3030
#[serde(deny_unknown_fields)]
3131
pub struct PrepareRequest {
32-
pub proposal_number: ProposalNumber,
32+
pub proposal_number: Option<ProposalNumber>,
3333
}
3434

3535
// Response type for the "prepare" endpoint
@@ -40,25 +40,30 @@ pub struct PrepareResponse {
4040
}
4141

4242
// Logic for the "prepare" endpoint
43-
fn prepare(request: &PrepareRequest, state: &mut State) -> PrepareResponse {
43+
fn prepare(
44+
request: &PrepareRequest,
45+
state: &mut (state::Durable, state::Volatile),
46+
) -> PrepareResponse {
4447
debug!(
4548
"Received prepare request:\n{}",
4649
serde_yaml::to_string(request).unwrap(), // Serialization is safe.
4750
);
4851

49-
match &state.min_proposal_number {
50-
Some(proposal_number) => {
51-
if request.proposal_number > *proposal_number {
52-
state.min_proposal_number = Some(request.proposal_number);
52+
if let Some(requested_proposal_number) = request.proposal_number {
53+
match &state.0.min_proposal_number {
54+
Some(proposal_number) => {
55+
if requested_proposal_number > *proposal_number {
56+
state.0.min_proposal_number = Some(requested_proposal_number);
57+
}
58+
}
59+
None => {
60+
state.0.min_proposal_number = Some(requested_proposal_number);
5361
}
54-
}
55-
None => {
56-
state.min_proposal_number = Some(request.proposal_number);
5762
}
5863
}
5964

6065
PrepareResponse {
61-
accepted_proposal: state.accepted_proposal.clone(),
66+
accepted_proposal: state.0.accepted_proposal.clone(),
6267
}
6368
}
6469

@@ -77,25 +82,30 @@ pub struct AcceptResponse {
7782
}
7883

7984
// Logic for the "accept" endpoint
80-
fn accept(request: &AcceptRequest, state: &mut State) -> AcceptResponse {
85+
fn accept(
86+
request: &AcceptRequest,
87+
state: &mut (state::Durable, state::Volatile),
88+
) -> AcceptResponse {
8189
debug!(
8290
"Received accept request:\n{}",
8391
serde_yaml::to_string(request).unwrap(), // Serialization is safe.
8492
);
93+
8594
if state
95+
.0
8696
.min_proposal_number
8797
.as_ref()
8898
.map_or(true, |proposal_number| {
8999
request.proposal.0 >= *proposal_number
90100
})
91101
{
92-
state.min_proposal_number = Some(request.proposal.0);
93-
state.accepted_proposal = Some(request.proposal.clone());
102+
state.0.min_proposal_number = Some(request.proposal.0);
103+
state.0.accepted_proposal = Some(request.proposal.clone());
94104
}
95105

96106
AcceptResponse {
97107
// The `unwrap` is safe since accepts must follow at least one prepare.
98-
min_proposal_number: state.min_proposal_number.unwrap(),
108+
min_proposal_number: state.0.min_proposal_number.unwrap(),
99109
}
100110
}
101111

@@ -112,20 +122,23 @@ pub struct ChooseRequest {
112122
pub struct ChooseResponse;
113123

114124
// Logic for the "choose" endpoint
115-
fn choose(request: &ChooseRequest, state: &mut State) -> ChooseResponse {
116-
if state.chosen_value.is_none() {
117-
info!("Consensus was achieved.");
125+
fn choose(
126+
request: &ChooseRequest,
127+
state: &mut (state::Durable, state::Volatile),
128+
) -> ChooseResponse {
129+
if state.1.chosen_value.is_none() {
130+
info!("Consensus achieved.");
118131
println!("{}", request.value);
119132
io::stdout().flush().unwrap_or(());
120-
state.chosen_value = Some(request.value.clone());
133+
state.1.chosen_value = Some(request.value.clone());
121134
}
122135
ChooseResponse {}
123136
}
124137

125138
// Context for each service instance
126139
#[derive(Clone)]
127140
struct Context {
128-
state: Arc<RwLock<State>>,
141+
state: Arc<RwLock<(state::Durable, state::Volatile)>>,
129142
data_file_path: PathBuf,
130143
}
131144

@@ -158,7 +171,7 @@ async fn handle_request(
158171
// Handle the request.
159172
let mut guard = context.state.write().await;
160173
let response = $endpoint(&payload, &mut guard);
161-
crate::state::write(&guard, &context.data_file_path).await?;
174+
crate::state::write(&guard.0, &context.data_file_path).await?;
162175

163176
// Serialize the response.
164177
Ok(Response::new(Body::from(
@@ -181,12 +194,15 @@ async fn handle_request(
181194

182195
// Summary of the program state
183196
(&Method::GET, "/") => {
184-
// Respond with a representation of the program state. The `unwrap`
185-
// is safe because serialization should never fail.
186-
let state_repr = serde_yaml::to_string(&*context.state.read().await).unwrap();
197+
// Respond with a representation of the program state. The `unwrap`s
198+
// are safe because serialization should never fail.
199+
let state = context.state.read().await;
200+
let durable_state_repr = serde_yaml::to_string(&state.0).unwrap();
201+
let volatile_state_repr = serde_yaml::to_string(&state.1).unwrap();
187202
Ok(Response::new(Body::from(format!(
188-
"System operational.\n\n{}",
189-
state_repr,
203+
"System operational.\n\nDurable state:\n\n{}\n\nVolatile state:\n\n{}",
204+
durable_state_repr,
205+
volatile_state_repr,
190206
))))
191207
}
192208

@@ -216,7 +232,7 @@ async fn handle_request(
216232

217233
// Entrypoint for the acceptor
218234
pub async fn acceptor(
219-
state: Arc<RwLock<State>>,
235+
state: Arc<RwLock<(state::Durable, state::Volatile)>>,
220236
data_file_path: &Path,
221237
address: SocketAddr,
222238
) -> Result<(), io::Error> {
@@ -257,49 +273,49 @@ mod tests {
257273
fn prepare_initializes_min_proposal_number() {
258274
let mut state = initial();
259275
let request = PrepareRequest {
260-
proposal_number: ProposalNumber {
276+
proposal_number: Some(ProposalNumber {
261277
round: 0,
262278
proposer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
263-
},
279+
}),
264280
};
265281
let response = prepare(&request, &mut state);
266-
assert_eq!(state.min_proposal_number, Some(request.proposal_number));
282+
assert_eq!(state.0.min_proposal_number, request.proposal_number);
267283
assert_eq!(response.accepted_proposal, None);
268284
}
269285

270286
#[test]
271287
fn prepare_increases_min_proposal_number() {
272288
let mut state = initial();
273-
state.min_proposal_number = Some(ProposalNumber {
289+
state.0.min_proposal_number = Some(ProposalNumber {
274290
round: 0,
275291
proposer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
276292
});
277293
let request = PrepareRequest {
278-
proposal_number: ProposalNumber {
294+
proposal_number: Some(ProposalNumber {
279295
round: 1,
280296
proposer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
281-
},
297+
}),
282298
};
283299
let response = prepare(&request, &mut state);
284-
assert_eq!(state.min_proposal_number, Some(request.proposal_number));
300+
assert_eq!(state.0.min_proposal_number, request.proposal_number);
285301
assert_eq!(response.accepted_proposal, None);
286302
}
287303

288304
#[test]
289305
fn prepare_does_not_decrease_min_proposal_number() {
290306
let mut state = initial();
291-
state.min_proposal_number = Some(ProposalNumber {
307+
state.0.min_proposal_number = Some(ProposalNumber {
292308
round: 1,
293309
proposer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
294310
});
295311
let request = PrepareRequest {
296-
proposal_number: ProposalNumber {
312+
proposal_number: Some(ProposalNumber {
297313
round: 0,
298314
proposer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
299-
},
315+
}),
300316
};
301317
let response = prepare(&request, &mut state);
302-
assert_ne!(state.min_proposal_number, Some(request.proposal_number));
318+
assert_ne!(state.0.min_proposal_number, request.proposal_number);
303319
assert_eq!(response.accepted_proposal, None);
304320
}
305321

@@ -313,13 +329,13 @@ mod tests {
313329
},
314330
"foo".to_string(),
315331
);
316-
state.min_proposal_number = Some(accepted_proposal.0);
317-
state.accepted_proposal = Some(accepted_proposal.clone());
332+
state.0.min_proposal_number = Some(accepted_proposal.0);
333+
state.0.accepted_proposal = Some(accepted_proposal.clone());
318334
let request = PrepareRequest {
319-
proposal_number: ProposalNumber {
335+
proposal_number: Some(ProposalNumber {
320336
round: 1,
321337
proposer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
322-
},
338+
}),
323339
};
324340
let response = prepare(&request, &mut state);
325341
assert_eq!(response.accepted_proposal, Some(accepted_proposal));
@@ -337,7 +353,7 @@ mod tests {
337353
);
338354

339355
let prepare_request = PrepareRequest {
340-
proposal_number: proposal.0,
356+
proposal_number: Some(proposal.0),
341357
};
342358
prepare(&prepare_request, &mut state);
343359

@@ -346,9 +362,9 @@ mod tests {
346362
};
347363
let accept_response = accept(&accept_request, &mut state);
348364

349-
assert_eq!(state.accepted_proposal, Some(proposal.clone()));
365+
assert_eq!(state.0.accepted_proposal, Some(proposal.clone()));
350366
assert_eq!(accept_response.min_proposal_number, proposal.0);
351-
assert_eq!(state.min_proposal_number, Some(proposal.0));
367+
assert_eq!(state.0.min_proposal_number, Some(proposal.0));
352368
}
353369

354370
#[test]
@@ -371,12 +387,12 @@ mod tests {
371387
);
372388

373389
let prepare_request1 = PrepareRequest {
374-
proposal_number: proposal0.0,
390+
proposal_number: Some(proposal0.0),
375391
};
376392
prepare(&prepare_request1, &mut state);
377393

378394
let prepare_request2 = PrepareRequest {
379-
proposal_number: proposal1.0,
395+
proposal_number: Some(proposal1.0),
380396
};
381397
prepare(&prepare_request2, &mut state);
382398

@@ -385,9 +401,9 @@ mod tests {
385401
};
386402
let accept_response = accept(&accept_request, &mut state);
387403

388-
assert_eq!(state.accepted_proposal, None);
404+
assert_eq!(state.0.accepted_proposal, None);
389405
assert_eq!(accept_response.min_proposal_number, proposal1.0);
390-
assert_eq!(state.min_proposal_number, Some(proposal1.0));
406+
assert_eq!(state.0.min_proposal_number, Some(proposal1.0));
391407
}
392408

393409
#[test]
@@ -397,6 +413,6 @@ mod tests {
397413
value: "foo".to_string(),
398414
};
399415
choose(&request, &mut state);
400-
assert_eq!(state.chosen_value, Some(request.value));
416+
assert_eq!(state.1.chosen_value, Some(request.value));
401417
}
402418
}

0 commit comments

Comments
 (0)