Skip to content

Conversation

@lynnagara
Copy link
Member

@lynnagara lynnagara commented Dec 18, 2025

This change does a few things:

  • simplifies the handler trait so it is no longer generic, and enables additional handlers to be added
  • adds a "health" handler stub -- this doesn't do anything yet except demonstrates that it is now possible
    to now add more than one handler.
  • finishes the merge responses implementation in the project configs handler

The generics solution does not work well for our use case since it requires the concrete type to be known at compile time. In our case the router picks the handler dynamically at runtime based on inspecting the incoming route.

With Arc<dyn Handler>, we can more easily dyanamically dispatch to the appropriate handler based on the matched route.

Each handler receives the raw requests/responses and is responsible for splitting and merging them as needed.

This chnage simplifies the handler trait so it is no longer generic.

The generics solution does not work well for our use case since
generics requires the concrete type to be known at compile time,
however in our case the router picks the handler dynamically at
runtime based on inspecting the incoming route.

With Arc<dyn Handler>, we can more easily dyanamically dispatch to
the appropriate handler based on the matched route.

Each handler receives the raw requests/responses and is responsible
for splitting and merging them as needed.
@lynnagara lynnagara requested a review from a team as a code owner December 18, 2025 21:14
Comment on lines 68 to 69
let (split, _metadata) =
handler.split_request(handler_req, &cells).await.unwrap();

This comment was marked as outdated.

Comment on lines +15 to +17
_request: Request<HandlerBody>,
_cells: &Cells,
) -> Result<(Vec<(CellId, Request<HandlerBody>)>, SplitMetadata), IngestRouterError> {

This comment was marked as outdated.

Comment on lines 112 to 115
// Downcast metadata to our specific type
if let Ok(project_metadata) = metadata.downcast::<ProjectConfigsMetadata>() {
merged.pending_keys.extend(*project_metadata);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is kind of the downside of this approach -- it's probably worth attempting to write a macro or enum here to avoid this need to manually downcast to the real type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a TODO for later

Comment on lines 394 to 395
assert!(pending.contains(&serde_json::json!("key_routing_failed")));
assert!(pending.contains(&serde_json::json!("key_from_failed_cell1")));

This comment was marked as outdated.

Comment on lines 469 to 473
let pending_from_split: ProjectConfigsMetadata = vec![
"key_routing_failed".to_string(),
"key_from_failed_cell1".to_string(),
"key_from_failed_cell2".to_string(),
];

This comment was marked as outdated.

#[derive(Default, Debug)]
struct ProjectConfigsMetadata {
// keys that are assigned to a cell
cell_to_keys: HashMap<CellId, Vec<String>>,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has to be added to the metadata -- if the request to a cell fails for any reason, we can look it up on this map and add the keys to pending during the merge phase

Comment on lines -94 to -99
// TODO: The current implementation does not handle errors from the results
// parameter. The edge case to be handled are if any of the upstreams failed
// to return a response for whatever reason. In scenarios like this, the
// executor needs to provide all the project config keys which failed to
// resolve on the upstream. We would need to add those project keys to the
// pending response.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is done now -- the split phase returns the cell to keys map, which allows us to figure out which keys are the failed ones from this function


let response = service.call(request).await.unwrap();

// TODO: call the scripts/mock_relay_api.py server and validate the response
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a note for later -- we should add a true end to end test here

Comment on lines +15 to +26
pub async fn deserialize_body<T: DeserializeOwned>(
body: HandlerBody,
) -> Result<T, IngestRouterError> {
let bytes = body.collect().await?.to_bytes();
serde_json::from_slice(&bytes).map_err(|e| IngestRouterError::RequestBodyError(e.to_string()))
}

/// Serializes a value to a JSON body.
pub fn serialize_to_body<T: Serialize>(value: &T) -> Result<HandlerBody, IngestRouterError> {
let bytes = serde_json::to_vec(value).map(Bytes::from)?;
Ok(Full::new(bytes).map_err(|e| match e {}).boxed())
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved these functions out of the project config implementation into utils as we can reuse them for any kind of json endpoint

}

/// Common header normalization for all requests and responses.
pub fn normalize_headers(headers: &mut HeaderMap, version: Version) -> &mut HeaderMap {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pulled this out as well, as we can reuse it for any relay endpoint in both directions - requests sent upstream and responses back

Comment on lines 141 to 145
if let Ok(parsed) = deserialize_body::<ProjectConfigsResponse>(body).await {
merged.project_configs.extend(parsed.project_configs);
merged.extra_fields.extend(parsed.extra_fields);
merged.pending_keys.extend(parsed.pending_keys);
}

This comment was marked as outdated.

/// Get the cells for a specific locale
pub fn get_cells(&self, locale: &str) -> Option<&Cells> {
self.locale_to_cells.get(locale)
pub fn get_cells(&self, locale: &str) -> Option<Arc<Cells>> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to Arc<Cells> since we pass Cells to a bunch of places now - and it's easier for the data to be owned

}
}

#[cfg(test)]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed these tests since ProjectConfigsRequest and ProjectConfigsResponse are barebones structs now -- i don't think there's too much value in testing builtin rust structures

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants