feat: Add helper functions to ApiKeyFactory struct#24
Conversation
f606cce to
f6c7a65
Compare
|
|
||
| #[derive(Clone)] | ||
| pub struct ApiKeyFactory { | ||
| inner: Arc<ApiKeyFactoryInner>, |
There was a problem hiding this comment.
Do you need this Arc? the ApiKeyResolverFn is already wrapped in an Arc, and the OnceCell should already be Send + Sync. Can you just derive Clone on ApiKeyFactoryInner? And then if you can do that, you no longer need the wrapper struct. You might even be able to get away with not having the Arc around the ApiKeyResolverFn, since you are already required it to be Send+Sync
There was a problem hiding this comment.
I might be wrong on OnceCell, it looks like it's memory is copied. You might be able to get away with just wrapping the OnceCell in an Arc, and that way at least you don't need the wrapper struct, and in the default case there is no overhead for using the static string.
There was a problem hiding this comment.
There is also LazyLock which might give the correct behaviour with less overhead https://doc.rust-lang.org/std/sync/struct.LazyLock.html
There was a problem hiding this comment.
1.
Do you need this Arc? the ApiKeyResolverFn is already wrapped in an Arc, and the OnceCell should already be Send + Sync. Can you just derive Clone on ApiKeyFactoryInner? And then if you can do that, you no longer need the wrapper struct.
Great! This works.
2.
You might even be able to get away with not having the Arc around the ApiKeyResolverFn, since you are already required it to be Send+Sync
Do you mean this?
pub type ApiKeyResolverFn = dyn Fn() -> Pin<Box<dyn Future<Output = String> + Send>> + Send + Sync;
#[derive(Clone)]
pub enum ApiKeyFactory {
Static(String),
Dynamic {
resolver_fn: ApiKeyResolverFn,
api_key: OnceCell<String>,
},
}
This doesn't work because resolver_fn has unknown size.
3.
I might be wrong on OnceCell, it looks like it's memory is copied. You might be able to get away with just wrapping the OnceCell in an Arc
You are right. Will add Arc.
4.
There is also LazyLock which might give the correct behaviour with less overhead
This doesn't work because std::sync::LazyLock doesn't allow async functions. OnceCell is the best choice I found that works inside tokio.
Thanks for the comments! I also just learned that enum can have impl.
There was a problem hiding this comment.
Pull Request Overview
This PR introduces a dedicated api_key module to encapsulate API key resolution logic and updates existing components to use the new ApiKeyFactory helpers.
- Moved
ApiKeyFactorylogic into its own module with static and dynamic constructors. - Refactored
Flusherto depend onApiKeyFactoryinstead of raw closures. - Updated integration tests and serverless-compat usage to call the new helpers.
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| crates/dogstatsd/tests/integration_test.rs | Swapped manual API key closure for ApiKeyFactory::new_from_static_key |
| crates/dogstatsd/src/lib.rs | Added pub mod api_key |
| crates/dogstatsd/src/flusher.rs | Updated Flusher to accept Arc<ApiKeyFactory> |
| crates/dogstatsd/src/api_key.rs | Added ApiKeyFactory enum, helper constructors, and unit tests |
| crates/datadog-serverless-compat/src/main.rs | Replaced inline API key closures with ApiKeyFactory::new_from_static_key |
Comments suppressed due to low confidence (2)
crates/dogstatsd/src/api_key.rs:37
- Add a unit test to verify that the dynamic resolver is only invoked once (i.e., that the OnceCell caching works as intended).
.get_or_init(|| async { (resolver_fn)().await })
crates/dogstatsd/src/api_key.rs:17
- [nitpick] Add module- or item-level doc comments for
ApiKeyFactory,new_from_resolver,new_from_static_key, andget_api_keyto clarify intended usage.
impl ApiKeyFactory {
| pub struct Flusher { | ||
| // Accept a future so the API key resolution is deferred until the flush happens | ||
| api_key_factory: ApiKeyFactory, | ||
| // Allow accepting a future so the API key resolution is deferred until the flush happens |
There was a problem hiding this comment.
[nitpick] Update this comment to reflect that api_key_factory now uses ApiKeyFactory rather than accepting a raw future-producing closure.
| // Allow accepting a future so the API key resolution is deferred until the flush happens | |
| // Uses ApiKeyFactory to defer API key resolution until the flush happens |
| } | ||
|
|
||
| impl ApiKeyFactory { | ||
| pub fn new_from_resolver(resolver_fn: ApiKeyResolverFn) -> Self { |
There was a problem hiding this comment.
Maybe some docs on what is considered a resolver
|
|
||
| #[cfg(test)] | ||
| pub mod tests { | ||
| use crate::api_key::ApiKeyFactory; |
There was a problem hiding this comment.
in tests. it's common to just use use super::*, which would give you all the imports from the file you're in
| use std::sync::Arc; | ||
|
|
||
| #[tokio::test] | ||
| async fn new_from_resolver() { |
There was a problem hiding this comment.
It's a good practice to name tests in the form of test_new_from_resolver to not confuse user to the usage above, sometimes, if you wanna be more descriptive and add more context, you can also do test_new_from_resolver_<EXPECTED-ACTION>
# Motivation From @astuyve: > today we basically block/await on that decrypt call before we can call /next so if we can instead make that async and then resolve the future only when we need to flush data, that can be a big win for many customers. https://datadoghq.atlassian.net/browse/SVLS-6995 # Previous work DataDog/serverless-components#21, DataDog/serverless-components#24 created `ApiKeyFactory`, which is a util to enable lazy API key resolution. # This PR Updates Bottlecap code to use `ApiKeyFactory` to lazily resolve API key, i.e. instead of resolving it by querying Secret Manager or KMS during init phase, do it at flushing time when api key is actually needed. # Note This PR changes the behavior when key resolution fails, i.e. when `resolve_secrets()` returns `None`. - Before: run `extension_loop_idle()`, which does not stop the runtime - After: panic, which will stop the runtime (if I understand correctly). Of course it's not ideal. Any better idea? - It's harder now to run `extension_loop_idle()` because api key resolution code is not in the main loop anymore, but in various consumer code of api key - Is there a way to gracefully shut down the extension without affecting the runtime? Update: Added a PR to address resolution failure: #732 These two PRs should be merged together. Keeping them separate PRs just to make review easier. # Testing ## Setup - Runtime: Go1 on Amazon Linux 2 - Architecture: arm64 - An app with empty implementation code ## Result Below is the `Datadog Next-Gen Extension ready in:` time logged. - Before: (prod extension `arn:aws:lambda:us-east-1:464622532012:layer:Datadog-Extension-ARM:82`) - 88.6 ± 1.8 (ms) - After: (test extension `arn:aws:lambda:us-east-1:425362996713:layer:Datadog-Bottlecap-Beta-ARM-yiming:2`) - 35.4 ± 5.1 (ms) - (-60.0%) <img width="461" alt="image" src="https://github.com/user-attachments/assets/b2973aae-d8f2-4003-a37f-6af05a42e059" /> Both use 5 samples. # Notes https://datadoghq.atlassian.net/issues/SVLS-6996 https://datadoghq.atlassian.net/issues/SVLS-6998
What does this PR do?
ApiKeyFactorystruct, including:new_from_resolver(), which takes in a resolver function and enables lazy resolutionnew_from_static_key(), which takes in a static api key string sliceget_api_key()ApiKeyFactoryrelated code to a separate moduleapi_keyMotivation
As a continuation of #21.
From @astuyve:
https://datadoghq.atlassian.net/browse/SVLS-6995
Additional Notes
Describe how to test/QA your changes