Skip to content

Commit 1bd0836

Browse files
authored
Merge pull request #14 from pact-foundation/fix/race-condition-match-result-recording
fix: record match results synchronously to eliminate race condition (#13)
2 parents 5b2b3df + 819ac3d commit 1bd0836

2 files changed

Lines changed: 39 additions & 28 deletions

File tree

pact_mock_server/src/hyper_server.rs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::collections::HashMap;
44
use std::fmt;
55
use std::fmt::{Display, Formatter};
66
use std::net::SocketAddr;
7-
#[cfg(feature = "tls")] use std::sync::Arc;
7+
use std::sync::{Arc, Mutex};
88
use std::time::Duration;
99

1010
#[allow(unused_imports)] use anyhow::anyhow;
@@ -72,7 +72,8 @@ pub(crate) async fn create_and_bind(
7272
server_id: String,
7373
pact: V4Pact,
7474
addr: SocketAddr,
75-
config: MockServerConfig
75+
config: MockServerConfig,
76+
matches: Arc<Mutex<Vec<MatchResult>>>
7677
) -> anyhow::Result<(SocketAddr, oneshot::Sender<()>, mpsc::Receiver<MockServerEvent>, JoinHandle<()>)> {
7778
let listener = TcpListener::bind(addr).await?;
7879
let local_addr = listener.local_addr()?;
@@ -88,6 +89,7 @@ pub(crate) async fn create_and_bind(
8889
let server_id = server_id.clone();
8990
let pact = pact.clone();
9091
let config = config.clone();
92+
let matches = matches.clone();
9193

9294
select! {
9395
connection = listener.accept() => {
@@ -106,9 +108,10 @@ pub(crate) async fn create_and_bind(
106108
let event_send = ev.clone();
107109
let config = config.clone();
108110
let server_id = sid.clone();
111+
let matches = matches.clone();
109112
LOG_ID.scope(server_id, async move {
110113
handle_mock_request_error(
111-
handle_request(req, pact.clone(), event_send.clone(), &local_addr, &config).await
114+
handle_request(req, pact.clone(), event_send.clone(), matches.clone(), &local_addr, &config).await
112115
)
113116
})
114117
})
@@ -162,7 +165,8 @@ pub(crate) async fn create_and_bind_https(
162165
server_id: String,
163166
pact: V4Pact,
164167
addr: SocketAddr,
165-
config: MockServerConfig
168+
config: MockServerConfig,
169+
matches: Arc<Mutex<Vec<MatchResult>>>
166170
) -> anyhow::Result<(SocketAddr, oneshot::Sender<()>, mpsc::Receiver<MockServerEvent>, JoinHandle<()>)> {
167171
if CryptoProvider::get_default().is_none() {
168172
warn!("No TLS cryptographic provider has been configured, defaulting to the standard FIPS provider");
@@ -197,6 +201,7 @@ pub(crate) async fn create_and_bind_https(
197201
let server_id = server_id.clone();
198202
let pact = pact.clone();
199203
let config = config.clone();
204+
let matches = matches.clone();
200205

201206
select! {
202207
connection = listener.accept() => {
@@ -219,9 +224,10 @@ pub(crate) async fn create_and_bind_https(
219224
let event_send = ev.clone();
220225
let config = config.clone();
221226
let server_id = sid.clone();
227+
let matches = matches.clone();
222228
LOG_ID.scope(server_id, async move {
223229
handle_mock_request_error(
224-
handle_request(req, pact.clone(), event_send.clone(), &local_addr, &config).await
230+
handle_request(req, pact.clone(), event_send.clone(), matches.clone(), &local_addr, &config).await
225231
)
226232
})
227233
})
@@ -278,6 +284,7 @@ async fn handle_request(
278284
req: Request<Incoming>,
279285
pact: V4Pact,
280286
event_send: Sender<MockServerEvent>,
287+
matches: Arc<Mutex<Vec<MatchResult>>>,
281288
local_addr: &SocketAddr,
282289
config: &MockServerConfig
283290
) -> Result<Response<Full<Bytes>>, InteractionError> {
@@ -322,8 +329,13 @@ async fn handle_request(
322329

323330
let match_result = match_request(&pact_request, &pact).await;
324331

325-
if let Err(_) = event_send.send(MockServerEvent::RequestMatch(match_result.clone())).await {
326-
error!("Failed to send RequestMatch event");
332+
// Record the match result synchronously before returning the response. This ensures that
333+
// pactffi_mock_server_matched() / pactffi_mock_server_mismatches() always sees the correct
334+
// state immediately after the client receives the HTTP response, with no race against the
335+
// async event loop.
336+
{
337+
let mut guard = matches.lock().unwrap();
338+
guard.push(match_result.clone());
327339
}
328340

329341
match_result_to_hyper_response(&pact_request, &match_result, local_addr, config).await
@@ -569,11 +581,13 @@ mod tests {
569581

570582
#[tokio::test]
571583
async fn can_fetch_results_on_current_thread() {
584+
let matches = Arc::new(Mutex::new(vec![]));
572585
let (_addr, shutdown, mut events, handle) = create_and_bind(
573586
"can_fetch_results_on_current_thread".to_string(),
574587
RequestResponsePact::default().as_v4_pact().unwrap(),
575588
([0, 0, 0, 0], 0u16).into(),
576-
MockServerConfig::default()
589+
MockServerConfig::default(),
590+
matches
577591
).await.unwrap();
578592

579593
shutdown.send(()).unwrap();
@@ -605,11 +619,13 @@ mod tests {
605619
interactions: vec![ RequestResponseInteraction::default() ],
606620
.. RequestResponsePact::default()
607621
};
622+
let matches = Arc::new(Mutex::new(vec![]));
608623
let (addr, shutdown, mut events, handle) = create_and_bind(
609624
"can_fetch_results_on_current_thread".to_string(),
610625
pact.as_v4_pact().unwrap(),
611626
([127, 0, 0, 1], 0u16).into(),
612-
MockServerConfig::default()
627+
MockServerConfig::default(),
628+
matches.clone()
613629
).await.unwrap();
614630

615631
let client = reqwest::ClientBuilder::new()
@@ -625,17 +641,18 @@ mod tests {
625641
shutdown.send(()).unwrap();
626642
let _ = handle.await;
627643

628-
// Should be at least 3 events
629-
expect!(events.len()).to(be_greater_or_equal_to(3));
644+
// Match results are now recorded directly in the mutex, not via the event channel.
645+
// Events should contain RequestReceived plus the shutdown event (and possibly a
646+
// ConnectionFailed on Linux once the server shuts down).
647+
expect!(events.len()).to(be_greater_or_equal_to(2));
630648
assert_eq!(events.recv().await.unwrap(), MockServerEvent::RequestReceived("/".to_string()));
631-
if let MockServerEvent::RequestMatch(_) = events.recv().await.unwrap() {
632-
// expected
633-
} else {
634-
panic!("Was expected a request match event");
635-
}
636649
// For some reason, a http2 connection returns an error once the server is shutdown on Linux
637650
let mut events_list = vec![];
638651
events.recv_many(&mut events_list, 2).await;
639652
assert_eq!(events_list.last().unwrap(), &MockServerEvent::ServerShutdown);
653+
654+
// Verify the match result was recorded synchronously in the mutex
655+
let guard = matches.lock().unwrap();
656+
assert_eq!(guard.len(), 1);
640657
}
641658
}

pact_mock_server/src/mock_server.rs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,6 @@ pub enum MockServerEvent {
210210
ConnectionFailed(String),
211211
/// Request received with path
212212
RequestReceived(String),
213-
/// Result of matching a request
214-
RequestMatch(MatchResult),
215213
/// Server is shutting down
216214
ServerShutdown
217215
}
@@ -295,15 +293,16 @@ impl MockServer {
295293
};
296294

297295
trace!(%server_id, %address, "Starting mock server");
298-
let (address, shutdown_send, event_recv, _task_handle) = create_and_bind(server_id.clone(), pact.clone(), address, config.clone()).await?;
296+
let matches = Arc::new(Mutex::new(vec![]));
297+
let (address, shutdown_send, event_recv, _task_handle) = create_and_bind(server_id.clone(), pact.clone(), address, config.clone(), matches.clone()).await?;
299298
trace!(%server_id, %address, "Mock server started");
300299

301300
let mut mock_server = MockServer {
302301
id: server_id,
303302
scheme: Default::default(),
304303
address,
305304
pact,
306-
matches: Default::default(),
305+
matches,
307306
shutdown_tx: RefCell::new(Some(shutdown_send)),
308307
config: config.clone(),
309308
metrics: Default::default(),
@@ -333,15 +332,16 @@ impl MockServer {
333332
};
334333

335334
trace!(%server_id, %address, "Starting TLS mock server");
336-
let (address, shutdown_send, event_recv, _task_handle) = create_and_bind_https(server_id.clone(), pact.clone(), address, config.clone()).await?;
335+
let matches = Arc::new(Mutex::new(vec![]));
336+
let (address, shutdown_send, event_recv, _task_handle) = create_and_bind_https(server_id.clone(), pact.clone(), address, config.clone(), matches.clone()).await?;
337337
trace!(%server_id, %address, "TLS mock server started");
338338

339339
let mut mock_server = MockServer {
340340
id: server_id,
341341
scheme: MockServerScheme::HTTPS,
342342
address,
343343
pact,
344-
matches: Default::default(),
344+
matches,
345345
shutdown_tx: RefCell::new(Some(shutdown_send)),
346346
config: config.clone(),
347347
metrics: Default::default(),
@@ -383,7 +383,6 @@ impl MockServer {
383383
fn start_event_loop(&mut self, mut event_recv: Receiver<MockServerEvent>) {
384384
let server_id = self.id.clone();
385385
let metrics = self.metrics.clone();
386-
let matches = self.matches.clone();
387386
let (sender, receiver) = mpsc::channel();
388387
self.event_loop_rx = Some(receiver);
389388

@@ -392,7 +391,6 @@ impl MockServer {
392391

393392
let mut total_events = 0;
394393
let metrics = metrics.clone();
395-
let matches = matches.clone();
396394
while let Some(event) = event_recv.recv().await {
397395
trace!(%server_id, ?event, "Received event");
398396
total_events += 1;
@@ -403,10 +401,6 @@ impl MockServer {
403401
let mut guard = metrics.lock().unwrap();
404402
guard.add_path(path);
405403
}
406-
MockServerEvent::RequestMatch(result) => {
407-
let mut guard = matches.lock().unwrap();
408-
guard.push(result.clone());
409-
}
410404
MockServerEvent::ServerShutdown => {
411405
trace!(%server_id, total_events, "Exiting mock server event loop");
412406
break;

0 commit comments

Comments
 (0)