Skip to content

Commit 0b99c11

Browse files
committed
fix: more optimal handling of resource paths
1 parent 9b93de0 commit 0b99c11

11 files changed

Lines changed: 381 additions & 83 deletions

File tree

sdk/cosmos/azure_data_cosmos/src/driver_bridge.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,20 @@ use crate::{
2424
/// Converts a driver [`DriverResponse`] into the SDK's typed [`CosmosResponse<T>`].
2525
///
2626
/// This reconstructs an `azure_core::Response<T>` from the driver's raw bytes,
27-
/// status code, and headers, then wraps it in the SDK's response type.
27+
/// status code, and headers, then wraps it in the SDK's response type using
28+
/// the pre-parsed headers from the driver to avoid a redundant parse.
2829
pub(crate) fn driver_response_to_cosmos_response<T>(
2930
driver_response: DriverResponse,
3031
) -> CosmosResponse<T> {
3132
let status_code: StatusCode = driver_response.status().status_code();
32-
let headers = driver_response_headers_to_headers(driver_response.headers());
33+
let cosmos_headers = driver_response.headers().clone();
34+
let headers = driver_response_headers_to_headers(&cosmos_headers);
3335
let body = driver_response.into_body();
3436

3537
let raw_response = RawResponse::from_bytes(status_code, headers, Bytes::from(body));
3638
let typed_response: Response<T> = raw_response.into();
3739

38-
CosmosResponse::from_response(typed_response)
40+
CosmosResponse::from_driver_response(typed_response, cosmos_headers)
3941
}
4042

4143
/// Converts driver [`CosmosResponseHeaders`] into raw [`Headers`] for the SDK response.

sdk/cosmos/azure_data_cosmos/src/models/cosmos_response.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,14 @@ impl<T> CosmosResponse<T> {
5050
}
5151
}
5252

53-
/// Creates a `CosmosResponse` from a typed response without a request.
53+
/// Creates a `CosmosResponse` from a typed response and pre-parsed headers.
5454
///
55-
/// Used for driver-routed operations where no `CosmosRequest` is available.
56-
pub(crate) fn from_response(response: Response<T>) -> Self {
57-
let cosmos_headers = CosmosResponseHeaders::from_headers(response.headers());
55+
/// Used by the driver bridge to avoid re-parsing headers that were already
56+
/// parsed by the driver pipeline.
57+
pub(crate) fn from_driver_response(
58+
response: Response<T>,
59+
cosmos_headers: CosmosResponseHeaders,
60+
) -> Self {
5861
let diagnostics = CosmosDiagnostics::from_headers(&cosmos_headers);
5962
Self {
6063
response,

sdk/cosmos/azure_data_cosmos_driver/src/driver/pipeline/components.rs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::{
1919
routing::{CosmosEndpoint, LocationIndex},
2020
transport::AuthorizationContext,
2121
},
22-
models::CosmosStatus,
22+
models::{CosmosResponseHeaders, CosmosStatus},
2323
options::Region,
2424
};
2525

@@ -296,12 +296,17 @@ impl TransportResult {
296296
///
297297
/// Successful status codes are mapped to `Success`; non-success status codes
298298
/// are mapped to `HttpError` with `request_sent` set to `Sent`.
299-
pub fn from_http_response(status: CosmosStatus, headers: Headers, body: Vec<u8>) -> Self {
299+
pub fn from_http_response(
300+
status: CosmosStatus,
301+
headers: Headers,
302+
cosmos_headers: CosmosResponseHeaders,
303+
body: Vec<u8>,
304+
) -> Self {
300305
if status.is_success() {
301306
Self {
302307
outcome: TransportOutcome::Success {
303308
status,
304-
headers,
309+
cosmos_headers,
305310
body,
306311
},
307312
}
@@ -310,37 +315,53 @@ impl TransportResult {
310315
outcome: TransportOutcome::HttpError {
311316
status,
312317
headers,
318+
cosmos_headers,
313319
body,
314320
request_sent: RequestSentStatus::Sent,
315321
},
316322
}
317323
}
318324
}
319325

320-
/// Returns the response headers if this is an HTTP response.
321-
pub fn response_headers(&self) -> Option<&Headers> {
326+
/// Returns the parsed Cosmos response headers if this is an HTTP response.
327+
pub fn cosmos_headers(&self) -> Option<&CosmosResponseHeaders> {
322328
match &self.outcome {
323-
TransportOutcome::Success { headers, .. } => Some(headers),
324-
TransportOutcome::HttpError { headers, .. } => Some(headers),
329+
TransportOutcome::Success { cosmos_headers, .. } => Some(cosmos_headers),
330+
TransportOutcome::HttpError { cosmos_headers, .. } => Some(cosmos_headers),
325331
TransportOutcome::TransportError { .. } | TransportOutcome::DeadlineExceeded { .. } => {
326332
None
327333
}
328334
}
329335
}
336+
337+
/// Returns the raw response headers for HTTP error responses.
338+
///
339+
/// Raw headers are only retained for error responses (needed to build a `RawResponse`
340+
/// for callers). For success responses, only parsed `CosmosResponseHeaders` are kept.
341+
pub fn response_headers(&self) -> Option<&Headers> {
342+
match &self.outcome {
343+
TransportOutcome::HttpError { headers, .. } => Some(headers),
344+
_ => None,
345+
}
346+
}
330347
}
331348

332349
/// The outcome of a single transport attempt.
333350
pub(crate) enum TransportOutcome {
334351
/// Successful response (2xx).
335352
Success {
336353
status: CosmosStatus,
337-
headers: Headers,
354+
/// Parsed Cosmos-specific response headers.
355+
cosmos_headers: CosmosResponseHeaders,
338356
body: Vec<u8>,
339357
},
340358
/// HTTP error response (non-2xx) that may be retryable at the operation level.
341359
HttpError {
342360
status: CosmosStatus,
361+
/// Raw headers retained for building `RawResponse` in error reporting.
343362
headers: Headers,
363+
/// Parsed Cosmos-specific response headers.
364+
cosmos_headers: CosmosResponseHeaders,
344365
body: Vec<u8>,
345366
request_sent: RequestSentStatus,
346367
},
@@ -371,12 +392,9 @@ impl std::fmt::Display for TransportOutcome {
371392
impl std::fmt::Debug for TransportOutcome {
372393
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373394
match self {
374-
TransportOutcome::Success {
375-
status, headers, ..
376-
} => f
395+
TransportOutcome::Success { status, .. } => f
377396
.debug_struct("Success")
378397
.field("status", status)
379-
.field("headers", headers)
380398
.field("body", &"...")
381399
.finish(),
382400
TransportOutcome::HttpError {

sdk/cosmos/azure_data_cosmos_driver/src/driver/pipeline/operation_pipeline.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use crate::{
1919
LocationStateStore,
2020
},
2121
models::{
22-
header_names, AccountEndpoint, ActivityId, CosmosOperation, CosmosResponse,
23-
CosmosResponseHeaders, Credential, DefaultConsistencyLevel, SessionToken, SubStatusCode,
22+
header_names, AccountEndpoint, ActivityId, CosmosOperation, CosmosResponse, Credential,
23+
DefaultConsistencyLevel, SessionToken, SubStatusCode,
2424
},
2525
options::{OperationOptionsView, ReadConsistencyStrategy},
2626
};
@@ -215,13 +215,12 @@ pub(crate) async fn execute_operation_pipeline(
215215
// variant does not carry headers — capturing after evaluation
216216
// would silently drop tokens from those responses.
217217
if session_consistency_active {
218-
if let Some(headers) = result.response_headers() {
219-
let cosmos_headers = CosmosResponseHeaders::from_headers(headers);
218+
if let Some(cosmos_headers) = result.cosmos_headers() {
220219
if should_capture_session_token_from_status(
221220
cosmos_headers.substatus.as_ref(),
222221
&result.outcome,
223222
) {
224-
session_manager.capture_session_token(operation, &cosmos_headers);
223+
session_manager.capture_session_token(operation, cosmos_headers);
225224
}
226225
}
227226
}
@@ -435,9 +434,11 @@ fn build_transport_request(
435434
resolved_session_token: Option<SessionToken>,
436435
) -> azure_core::Result<TransportRequest> {
437436
let resource_ref = operation.resource_reference();
438-
let request_path = resource_ref.request_path();
437+
// Compute both paths in a single pass with a single allocation.
438+
let paths = resource_ref.compute_paths();
439439
let url = {
440440
let mut base = routing.selected_url.clone();
441+
let request_path = paths.request_path();
441442
let normalized = if request_path.starts_with('/') {
442443
request_path.to_string()
443444
} else if request_path.is_empty() {
@@ -451,10 +452,9 @@ fn build_transport_request(
451452

452453
let method = operation.operation_type().http_method();
453454
let resource_type = operation.resource_type();
454-
let resource_link = resource_ref.link_for_signing();
455-
let signing_link = resource_link.trim_start_matches('/');
456-
457-
let auth_context = AuthorizationContext::new(method, resource_type, signing_link);
455+
// Move `paths` into AuthorizationContext so the signing link is a zero-copy
456+
// sub-slice of the path buffer — no additional string allocation needed.
457+
let auth_context = AuthorizationContext::from_paths(method, resource_type, paths);
458458

459459
// Build headers from the operation.
460460
// Custom headers are inserted first so that SDK-set headers below always
@@ -526,10 +526,9 @@ fn build_cosmos_response(
526526
match result.outcome {
527527
TransportOutcome::Success {
528528
status,
529-
headers,
529+
cosmos_headers,
530530
body,
531531
} => {
532-
let cosmos_headers = CosmosResponseHeaders::from_headers(&headers);
533532
diagnostics.set_operation_status(status.status_code(), status.sub_status());
534533

535534
let diagnostics_ctx = Arc::new(diagnostics.complete());
@@ -1002,15 +1001,15 @@ mod tests {
10021001

10031002
use crate::{
10041003
driver::pipeline::components::TransportOutcome,
1005-
models::{CosmosStatus, SubStatusCode},
1004+
models::{CosmosResponseHeaders, CosmosStatus, SubStatusCode},
10061005
};
10071006

10081007
use super::super::should_capture_session_token_from_status;
10091008

10101009
fn success_outcome() -> TransportOutcome {
10111010
TransportOutcome::Success {
10121011
status: CosmosStatus::new(StatusCode::Ok),
1013-
headers: Headers::new(),
1012+
cosmos_headers: CosmosResponseHeaders::default(),
10141013
body: Vec::new(),
10151014
}
10161015
}
@@ -1019,6 +1018,7 @@ mod tests {
10191018
TransportOutcome::HttpError {
10201019
status: CosmosStatus::new(status),
10211020
headers: Headers::new(),
1021+
cosmos_headers: CosmosResponseHeaders::default(),
10221022
body: Vec::new(),
10231023
request_sent: crate::diagnostics::RequestSentStatus::Sent,
10241024
}

sdk/cosmos/azure_data_cosmos_driver/src/driver/pipeline/retry_evaluation.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub(crate) fn evaluate_transport_result(
4646
headers,
4747
body,
4848
request_sent,
49+
..
4950
} => {
5051
let request_definitely_not_sent = request_sent.definitely_not_sent();
5152

@@ -284,7 +285,10 @@ mod tests {
284285
use super::*;
285286
use crate::{
286287
diagnostics::RequestSentStatus,
287-
models::{AccountReference, CosmosOperation, CosmosStatus, DatabaseReference},
288+
models::{
289+
AccountReference, CosmosOperation, CosmosResponseHeaders, CosmosStatus,
290+
DatabaseReference,
291+
},
288292
};
289293
use azure_core::http::StatusCode;
290294

@@ -310,7 +314,7 @@ mod tests {
310314
TransportResult {
311315
outcome: TransportOutcome::Success {
312316
status: CosmosStatus::new(StatusCode::Ok),
313-
headers: azure_core::http::headers::Headers::new(),
317+
cosmos_headers: CosmosResponseHeaders::default(),
314318
body: b"{}".to_vec(),
315319
},
316320
}
@@ -334,6 +338,7 @@ mod tests {
334338
outcome: TransportOutcome::HttpError {
335339
status: CosmosStatus::new(status_code),
336340
headers: azure_core::http::headers::Headers::new(),
341+
cosmos_headers: CosmosResponseHeaders::default(),
337342
body: vec![],
338343
request_sent: RequestSentStatus::Sent,
339344
},
@@ -482,6 +487,7 @@ mod tests {
482487
outcome: TransportOutcome::HttpError {
483488
status: CosmosStatus::WRITE_FORBIDDEN,
484489
headers: azure_core::http::headers::Headers::new(),
490+
cosmos_headers: CosmosResponseHeaders::default(),
485491
body: vec![],
486492
request_sent: RequestSentStatus::Sent,
487493
},
@@ -505,6 +511,7 @@ mod tests {
505511
outcome: TransportOutcome::HttpError {
506512
status: CosmosStatus::READ_SESSION_NOT_AVAILABLE,
507513
headers: azure_core::http::headers::Headers::new(),
514+
cosmos_headers: CosmosResponseHeaders::default(),
508515
body: vec![],
509516
request_sent: RequestSentStatus::Sent,
510517
},

sdk/cosmos/azure_data_cosmos_driver/src/driver/transport/authorization_policy.rs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,55 @@ use crate::models::{Credential, ResourceType};
1010
use azure_core::http::Method;
1111
use tracing::trace;
1212

13+
use crate::models::ResourcePaths;
14+
1315
/// Cosmos DB AAD scope for token authentication.
1416
const COSMOS_AAD_SCOPE: &str = "https://cosmos.azure.com/.default";
1517

18+
/// The resource link used when signing a Cosmos DB request.
19+
///
20+
/// `Paths` owns a [`ResourcePaths`] so the signing link is derived as a
21+
/// zero-copy sub-slice of the pre-computed path buffer (the hot path).
22+
/// `Owned` holds an independently allocated `String` for call sites that
23+
/// construct an `AuthorizationContext` outside of the normal request pipeline.
24+
pub(crate) enum ResourceLink {
25+
/// Signing link is derived from the pre-computed [`ResourcePaths`] buffer.
26+
Paths(ResourcePaths),
27+
/// Signing link is an independently owned string.
28+
Owned(String),
29+
}
30+
31+
impl ResourceLink {
32+
pub(crate) fn as_str(&self) -> &str {
33+
match self {
34+
Self::Paths(p) => p.signing_link(),
35+
Self::Owned(s) => s.as_str(),
36+
}
37+
}
38+
}
39+
40+
impl std::fmt::Debug for ResourceLink {
41+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42+
f.write_str(self.as_str())
43+
}
44+
}
45+
1646
/// Authorization context needed to build a Cosmos DB signature.
17-
#[derive(Debug, Clone)]
47+
#[derive(Debug)]
1848
pub(crate) struct AuthorizationContext {
1949
/// The HTTP method of the request.
2050
pub(crate) method: Method,
2151
/// The resource type being accessed.
2252
pub(crate) resource_type: ResourceType,
2353
/// The resource link for signing (path without leading slash, unencoded).
24-
pub(crate) resource_link: String,
54+
pub(crate) resource_link: ResourceLink,
2555
}
2656

2757
impl AuthorizationContext {
28-
/// Creates a new authorization context.
58+
/// Creates a new authorization context with an owned resource link string.
59+
///
60+
/// Use [`AuthorizationContext::from_paths`] on the hot path to avoid copying
61+
/// the signing link out of the pre-computed [`ResourcePaths`].
2962
pub(crate) fn new(
3063
method: Method,
3164
resource_type: ResourceType,
@@ -34,7 +67,21 @@ impl AuthorizationContext {
3467
Self {
3568
method,
3669
resource_type,
37-
resource_link: resource_link.into(),
70+
resource_link: ResourceLink::Owned(resource_link.into()),
71+
}
72+
}
73+
74+
/// Creates a new authorization context that derives the signing link directly
75+
/// from `paths`, avoiding any additional string allocation.
76+
pub(crate) fn from_paths(
77+
method: Method,
78+
resource_type: ResourceType,
79+
paths: ResourcePaths,
80+
) -> Self {
81+
Self {
82+
method,
83+
resource_type,
84+
resource_link: ResourceLink::Paths(paths),
3885
}
3986
}
4087
}
@@ -82,7 +129,7 @@ fn build_string_to_sign(auth_ctx: &AuthorizationContext, date_string: &str) -> S
82129
"{}\n{}\n{}\n{}\n\n",
83130
method_str,
84131
auth_ctx.resource_type.path_segment(),
85-
auth_ctx.resource_link,
132+
auth_ctx.resource_link.as_str(),
86133
date_string,
87134
)
88135
}

0 commit comments

Comments
 (0)