Skip to content

Commit 372b67b

Browse files
jordanhunt22Convex, Inc.
authored and
Convex, Inc.
committed
[Http actions -> FunRun] Add new RPC to support running Http actions (#29944)
GitOrigin-RevId: 57c6c4c7749511a1132c0f7c3c4813de6e418361
1 parent b888c98 commit 372b67b

File tree

11 files changed

+300
-57
lines changed

11 files changed

+300
-57
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/function_runner/src/server.rs

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ pub struct RunRequestArgs {
148148
pub context: ExecutionContext,
149149
}
150150

151+
#[derive(Clone)]
151152
pub struct FunctionMetadata {
152153
pub path_and_args: ValidatedPathAndArgs,
153154
pub journal: QueryJournal,
@@ -376,37 +377,19 @@ impl<RT: Runtime, S: StorageForInstance<RT>> FunctionRunnerCore<RT, S> {
376377
pub async fn run_function_no_retention_check(
377378
&self,
378379
run_request_args: RunRequestArgs,
379-
function_metadata: FunctionMetadata,
380+
function_metadata: Option<FunctionMetadata>,
381+
http_action_metadata: Option<HttpActionMetadata>,
380382
) -> anyhow::Result<(
381383
Option<FunctionFinalTransaction>,
382384
FunctionOutcome,
383385
FunctionUsageStats,
384386
)> {
385-
if run_request_args.udf_type == UdfType::HttpAction {
386-
anyhow::bail!("You can't run http actions from this method");
387-
}
388-
self.run_function_no_retention_check_inner(run_request_args, Some(function_metadata), None)
389-
.await
390-
}
391-
392-
#[minitrace::trace]
393-
pub async fn run_http_action_no_retention_check(
394-
&self,
395-
run_request_args: RunRequestArgs,
396-
http_action_metadata: HttpActionMetadata,
397-
) -> anyhow::Result<(FunctionOutcome, FunctionUsageStats)> {
398-
if run_request_args.udf_type != UdfType::HttpAction {
399-
anyhow::bail!("You can only run http actions with this method");
400-
}
401-
let (_, outcome, stats) = self
402-
.run_function_no_retention_check_inner(
403-
run_request_args,
404-
None,
405-
Some(http_action_metadata),
406-
)
407-
.await?;
408-
409-
Ok((outcome, stats))
387+
self.run_function_no_retention_check_inner(
388+
run_request_args,
389+
function_metadata,
390+
http_action_metadata,
391+
)
392+
.await
410393
}
411394

412395
#[minitrace::trace]
@@ -739,22 +722,13 @@ impl<RT: Runtime> FunctionRunner<RT> for InProcessFunctionRunner<RT> {
739722
let result = match udf_type {
740723
UdfType::Query | UdfType::Mutation | UdfType::Action => {
741724
self.server
742-
.run_function_no_retention_check(
743-
request_metadata,
744-
function_metadata
745-
.context("Missing function metadata for query, mutation or action")?,
746-
)
725+
.run_function_no_retention_check(request_metadata, function_metadata, None)
747726
.await
748727
},
749728
UdfType::HttpAction => {
750-
let (outcome, stats) = self
751-
.server
752-
.run_http_action_no_retention_check(
753-
request_metadata,
754-
http_action_metadata.context("Missing http action metadata")?,
755-
)
756-
.await?;
757-
Ok((None, outcome, stats))
729+
self.server
730+
.run_function_no_retention_check(request_metadata, None, http_action_metadata)
731+
.await
758732
},
759733
};
760734
validate_run_function_result(udf_type, *ts, self.database.retention_validator()).await?;

crates/isolate/src/environment/action/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ use crate::{
182182
};
183183

184184
#[derive(Debug, Clone)]
185+
#[cfg_attr(
186+
any(test, feature = "testing"),
187+
derive(proptest_derive::Arbitrary, PartialEq)
188+
)]
185189
pub enum HttpActionResult {
186190
Streamed,
187191
Error(JsError),

crates/isolate/src/environment/action/outcome.rs

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@ use pb::{
1616
function_result::Result as FunctionResultTypeProto,
1717
FunctionResult as FunctionResultProto,
1818
},
19-
outcome::ActionOutcome as ActionOutcomeProto,
19+
outcome::{
20+
ActionOutcome as ActionOutcomeProto,
21+
HttpActionOutcome as HttpActionOutcomeProto,
22+
},
2023
};
2124
#[cfg(any(test, feature = "testing"))]
2225
use proptest::prelude::*;
26+
use semver::Version;
2327
use serde_json::Value as JsonValue;
2428
use value::ConvexValue;
2529

2630
use super::HttpActionResult;
31+
#[cfg(any(test, feature = "testing"))]
32+
use crate::HttpActionRequest;
2733
use crate::{
2834
environment::helpers::{
2935
JsonPackedValue,
@@ -164,6 +170,7 @@ impl Arbitrary for ActionOutcome {
164170
}
165171

166172
#[derive(Debug, Clone)]
173+
#[cfg_attr(any(test, feature = "testing"), derive(PartialEq))]
167174
pub struct HttpActionOutcome {
168175
pub route: HttpActionRoute,
169176
pub http_request: HttpActionRequestHead,
@@ -207,6 +214,108 @@ impl HttpActionOutcome {
207214
pub fn memory_in_mb(&self) -> u64 {
208215
self.memory_in_mb
209216
}
217+
218+
pub(crate) fn from_proto(
219+
HttpActionOutcomeProto {
220+
unix_timestamp,
221+
result,
222+
syscall_trace,
223+
memory_in_mb,
224+
}: HttpActionOutcomeProto,
225+
http_request: HttpActionRequestHead,
226+
udf_server_version: Option<Version>,
227+
identity: InertIdentity,
228+
) -> anyhow::Result<Self> {
229+
let result = result.ok_or_else(|| anyhow::anyhow!("Missing result"))?;
230+
let result = match result.result {
231+
Some(FunctionResultTypeProto::JsonPackedValue(_)) => {
232+
anyhow::bail!("Http actions not expected to have aresult")
233+
},
234+
Some(FunctionResultTypeProto::JsError(js_error)) => {
235+
HttpActionResult::Error(js_error.try_into()?)
236+
},
237+
None => HttpActionResult::Streamed,
238+
};
239+
Ok(Self {
240+
identity,
241+
unix_timestamp: unix_timestamp
242+
.context("Missing unix_timestamp")?
243+
.try_into()?,
244+
result,
245+
syscall_trace: syscall_trace.context("Missing syscall_trace")?.try_into()?,
246+
memory_in_mb,
247+
http_request: http_request.clone(),
248+
udf_server_version,
249+
route: HttpActionRoute {
250+
method: http_request.method.try_into()?,
251+
path: http_request.url.to_string(),
252+
},
253+
})
254+
}
255+
}
256+
257+
impl TryFrom<HttpActionOutcome> for HttpActionOutcomeProto {
258+
type Error = anyhow::Error;
259+
260+
fn try_from(
261+
HttpActionOutcome {
262+
route: _,
263+
http_request: _,
264+
identity: _,
265+
unix_timestamp,
266+
result,
267+
syscall_trace,
268+
udf_server_version: _,
269+
memory_in_mb,
270+
}: HttpActionOutcome,
271+
) -> anyhow::Result<Self> {
272+
let result = match result {
273+
HttpActionResult::Streamed => None,
274+
HttpActionResult::Error(js_error) => {
275+
Some(FunctionResultTypeProto::JsError(js_error.try_into()?))
276+
},
277+
};
278+
Ok(Self {
279+
unix_timestamp: Some(unix_timestamp.into()),
280+
result: Some(FunctionResultProto { result }),
281+
syscall_trace: Some(syscall_trace.try_into()?),
282+
memory_in_mb,
283+
})
284+
}
285+
}
286+
287+
#[cfg(any(test, feature = "testing"))]
288+
impl Arbitrary for HttpActionOutcome {
289+
type Parameters = ();
290+
291+
type Strategy = impl Strategy<Value = HttpActionOutcome>;
292+
293+
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
294+
(
295+
any::<HttpActionRequest>(),
296+
any::<HttpActionResult>(),
297+
any::<InertIdentity>(),
298+
any::<UnixTimestamp>(),
299+
any::<SyscallTrace>(),
300+
any::<u64>(),
301+
)
302+
.prop_map(
303+
|(request, result, identity, unix_timestamp, syscall_trace, memory_in_mb)| Self {
304+
http_request: request.head.clone(),
305+
result,
306+
route: HttpActionRoute {
307+
method: request.head.method.try_into().unwrap(),
308+
path: request.head.url.to_string(),
309+
},
310+
identity,
311+
unix_timestamp,
312+
syscall_trace,
313+
memory_in_mb,
314+
// Ok to not generate semver::Version because it is not serialized anyway
315+
udf_server_version: None,
316+
},
317+
)
318+
}
210319
}
211320

212321
#[cfg(test)]
@@ -216,8 +325,10 @@ mod tests {
216325
use super::{
217326
ActionOutcome,
218327
ActionOutcomeProto,
328+
HttpActionOutcomeProto,
219329
ValidatedPathAndArgs,
220330
};
331+
use crate::HttpActionOutcome;
221332

222333
proptest! {
223334
#![proptest_config(
@@ -244,5 +355,21 @@ mod tests {
244355
).unwrap();
245356
assert_eq!(udf_outcome, udf_outcome_from_proto);
246357
}
358+
359+
#[test]
360+
fn test_http_action_outcome_roundtrips(udf_outcome in any::<HttpActionOutcome>()) {
361+
let udf_outcome_clone = udf_outcome.clone();
362+
let http_request = udf_outcome.http_request.clone();
363+
let version = udf_outcome.udf_server_version.clone();
364+
let identity = udf_outcome_clone.identity.clone();
365+
let proto = HttpActionOutcomeProto::try_from(udf_outcome_clone).unwrap();
366+
let udf_outcome_from_proto = HttpActionOutcome::from_proto(
367+
proto,
368+
http_request,
369+
version,
370+
identity,
371+
).unwrap();
372+
assert_eq!(udf_outcome, udf_outcome_from_proto);
373+
}
247374
}
248375
}

crates/isolate/src/environment/helpers/outcome.rs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use anyhow::Context;
12
use common::identity::InertIdentity;
23
use pb::outcome::{
34
function_outcome::Outcome as OutcomeProto,
@@ -12,6 +13,8 @@ use crate::{
1213
},
1314
udf::outcome::UdfOutcome,
1415
},
16+
HttpActionRequestHead,
17+
ValidatedHttpPath,
1518
ValidatedPathAndArgs,
1619
};
1720

@@ -32,22 +35,40 @@ pub enum FunctionOutcome {
3235
impl FunctionOutcome {
3336
pub fn from_proto(
3437
FunctionOutcomeProto { outcome }: FunctionOutcomeProto,
35-
path_and_args: ValidatedPathAndArgs,
38+
path_and_args: Option<ValidatedPathAndArgs>,
39+
http_metadata: Option<(ValidatedHttpPath, HttpActionRequestHead)>,
3640
identity: InertIdentity,
3741
) -> anyhow::Result<Self> {
3842
let outcome = outcome.ok_or_else(|| anyhow::anyhow!("Missing outcome"))?;
3943
match outcome {
4044
OutcomeProto::Query(outcome) => Ok(FunctionOutcome::Query(UdfOutcome::from_proto(
4145
outcome,
42-
path_and_args,
46+
path_and_args.context("Missing path and args")?,
4347
identity,
4448
)?)),
45-
OutcomeProto::Mutation(outcome) => Ok(FunctionOutcome::Mutation(
46-
UdfOutcome::from_proto(outcome, path_and_args, identity)?,
47-
)),
48-
OutcomeProto::Action(outcome) => Ok(FunctionOutcome::Action(
49-
ActionOutcome::from_proto(outcome, path_and_args, identity)?,
50-
)),
49+
OutcomeProto::Mutation(outcome) => {
50+
Ok(FunctionOutcome::Mutation(UdfOutcome::from_proto(
51+
outcome,
52+
path_and_args.context("Missing path and args")?,
53+
identity,
54+
)?))
55+
},
56+
OutcomeProto::Action(outcome) => {
57+
Ok(FunctionOutcome::Action(ActionOutcome::from_proto(
58+
outcome,
59+
path_and_args.context("Missing path and args")?,
60+
identity,
61+
)?))
62+
},
63+
OutcomeProto::HttpAction(outcome) => {
64+
let (http_path, http_request) = http_metadata.context("Missing http metadata")?;
65+
Ok(FunctionOutcome::HttpAction(HttpActionOutcome::from_proto(
66+
outcome,
67+
http_request,
68+
http_path.npm_version().clone(),
69+
identity,
70+
)?))
71+
},
5172
}
5273
}
5374
}
@@ -60,9 +81,7 @@ impl TryFrom<FunctionOutcome> for FunctionOutcomeProto {
6081
FunctionOutcome::Query(outcome) => OutcomeProto::Query(outcome.try_into()?),
6182
FunctionOutcome::Mutation(outcome) => OutcomeProto::Mutation(outcome.try_into()?),
6283
FunctionOutcome::Action(outcome) => OutcomeProto::Action(outcome.try_into()?),
63-
FunctionOutcome::HttpAction(_) => {
64-
anyhow::bail!("Funrun does not support http actions")
65-
},
84+
FunctionOutcome::HttpAction(outcome) => OutcomeProto::HttpAction(outcome.try_into()?),
6685
};
6786
Ok(FunctionOutcomeProto {
6887
outcome: Some(outcome),

0 commit comments

Comments
 (0)