runtime: avoid ticket backend blocking panic

This commit is contained in:
Keisuke Hirata 2026-07-20 09:45:39 +09:00
parent 2b0901eb62
commit 18d9b96cac
No known key found for this signature in database
2 changed files with 32 additions and 6 deletions

View File

@ -313,7 +313,6 @@ impl FeatureModule for TicketFeature {
struct WorkspaceHttpTicketBackend { struct WorkspaceHttpTicketBackend {
workspace_id: String, workspace_id: String,
base_url: String, base_url: String,
client: reqwest::blocking::Client,
} }
impl WorkspaceHttpTicketBackend { impl WorkspaceHttpTicketBackend {
@ -321,7 +320,6 @@ impl WorkspaceHttpTicketBackend {
Self { Self {
workspace_id, workspace_id,
base_url: base_url.trim_end_matches('/').to_string(), base_url: base_url.trim_end_matches('/').to_string(),
client: reqwest::blocking::Client::new(),
} }
} }
@ -335,13 +333,27 @@ impl WorkspaceHttpTicketBackend {
fn invoke( fn invoke(
&self, &self,
operation: TicketBackendOperation, operation: TicketBackendOperation,
) -> TicketResult<TicketBackendOperationResult> {
let endpoint = self.endpoint();
if tokio::runtime::Handle::try_current().is_ok() {
return std::thread::spawn(move || Self::invoke_http(endpoint, operation))
.join()
.map_err(|_| {
TicketError::Conflict("ticket backend request thread panicked".to_string())
})?;
}
Self::invoke_http(endpoint, operation)
}
fn invoke_http(
endpoint: String,
operation: TicketBackendOperation,
) -> TicketResult<TicketBackendOperationResult> { ) -> TicketResult<TicketBackendOperationResult> {
let body = serde_json::to_string(&operation).map_err(|error| { let body = serde_json::to_string(&operation).map_err(|error| {
TicketError::Conflict(format!("serialize ticket operation: {error}")) TicketError::Conflict(format!("serialize ticket operation: {error}"))
})?; })?;
let response = self let response = reqwest::blocking::Client::new()
.client .post(endpoint)
.post(self.endpoint())
.header(reqwest::header::CONTENT_TYPE, "application/json") .header(reqwest::header::CONTENT_TYPE, "application/json")
.body(body) .body(body)
.send() .send()
@ -981,6 +993,20 @@ provider = "github"
assert!(!root.join("closed").exists()); assert!(!root.join("closed").exists());
} }
#[tokio::test(flavor = "multi_thread")]
async fn workspace_http_backend_invoke_is_safe_inside_async_context() {
let backend =
WorkspaceHttpTicketBackend::new("workspace-a".to_string(), "not-a-url".to_string());
let error = backend
.invoke(TicketBackendOperation::DefaultIntakeReadyStateChangeBody {
from: "planning".to_string(),
})
.unwrap_err();
assert!(error.to_string().contains("ticket backend request failed"));
}
#[test] #[test]
fn workspace_http_backend_executes_ticket_create_operation() { fn workspace_http_backend_executes_ticket_create_operation() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let listener = TcpListener::bind("127.0.0.1:0").unwrap();

View File

@ -318,7 +318,7 @@ start_runtime() {
return 1 return 1
fi fi
start_service runtime "$ROOT_DIR" "$port" \ start_service runtime "$ROOT_DIR" "$port" \
"$runtime_bin" --bind "$RUNTIME_BIND" env RUST_BACKTRACE="${RUST_BACKTRACE:-1}" "$runtime_bin" --bind "$RUNTIME_BIND"
} }
start_backend() { start_backend() {