Skip to content

Commit 8c045d2

Browse files
authored
Add support for TimeStreamWrite and TimeStreamQuery (#2707)
TODO: - [x] docs - [x] integration test (canary even?) - [x] customize README for timestream ## Motivation and Context - #613 - awslabs/aws-sdk-rust#114 ## Description This adds support for TSW and TSQ by adding endpoint discovery as a customization. This is made much simpler by the fact that endpoint discovery for these services **has no parameters** which means that there is no complexity from caching the returned endpoint. Customers call `.enable_endpoint_discovery()` on the client to create a version of the client with endpoint discovery enabled. This returns a new client and a Reloader from which customers must spawn the reload task if they want endpoint discovery to rerun. ## Testing <!--- Please describe in detail how you tested your changes --> <!--- Include details of your testing environment, and the tests you ran to --> <!--- see how your change affects other areas of the code, etc. --> ## Checklist <!--- If a checkbox below is not applicable, then please DELETE it rather than leaving it unchecked --> - [ ] I have updated `CHANGELOG.next.toml` if I made changes to the smithy-rs codegen or runtime crates - [ ] I have updated `CHANGELOG.next.toml` if I made changes to the AWS SDK, generated SDK code, or SDK runtime crates ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 7347c58 commit 8c045d2

File tree

23 files changed

+7717
-19
lines changed

23 files changed

+7717
-19
lines changed

CHANGELOG.next.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,9 @@ no longer requires `Service::Error = OperationError<Op::Error, PollError>`, inst
115115
references = ["smithy-rs#2457"]
116116
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "server" }
117117
author = "hlbarber"
118+
119+
[[aws-sdk-rust]]
120+
message = "The SDK has added support for timestreamwrite and timestreamquery. Support for these services is considered experimental at this time. In order to use these services, you MUST call `.enable_endpoint_discovery()` on the `Client` after construction."
121+
meta = { "breaking" = false, "tada" = true, "bug" = false }
122+
references = ["smithy-rs#2707", "aws-sdk-rust#114"]
123+
author = "rcoh"

aws/rust-runtime/aws-inlineable/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" }
2424
aws-smithy-http-tower = { path = "../../../rust-runtime/aws-smithy-http-tower" }
2525
aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api" }
2626
aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" }
27+
aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async" }
2728
aws-types = { path = "../aws-types" }
2829
bytes = "1"
2930
bytes-utils = "0.1.1"
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
//! Maintain a cache of discovered endpoints
7+
8+
use aws_smithy_async::rt::sleep::AsyncSleep;
9+
use aws_smithy_async::time::TimeSource;
10+
use aws_smithy_client::erase::boxclone::BoxFuture;
11+
use aws_smithy_http::endpoint::{ResolveEndpoint, ResolveEndpointError};
12+
use aws_smithy_types::endpoint::Endpoint;
13+
use std::fmt::{Debug, Formatter};
14+
use std::future::Future;
15+
use std::sync::{Arc, Mutex};
16+
use std::time::{Duration, SystemTime};
17+
use tokio::sync::oneshot::error::TryRecvError;
18+
use tokio::sync::oneshot::{Receiver, Sender};
19+
20+
/// Endpoint reloader
21+
#[must_use]
22+
pub struct ReloadEndpoint {
23+
loader: Box<dyn Fn() -> BoxFuture<(Endpoint, SystemTime), ResolveEndpointError> + Send + Sync>,
24+
endpoint: Arc<Mutex<Option<ExpiringEndpoint>>>,
25+
error: Arc<Mutex<Option<ResolveEndpointError>>>,
26+
rx: Receiver<()>,
27+
sleep: Arc<dyn AsyncSleep>,
28+
time: Arc<dyn TimeSource>,
29+
}
30+
31+
impl Debug for ReloadEndpoint {
32+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
33+
f.debug_struct("ReloadEndpoint").finish()
34+
}
35+
}
36+
37+
impl ReloadEndpoint {
38+
/// Reload the endpoint once
39+
pub async fn reload_once(&self) {
40+
match (self.loader)().await {
41+
Ok((endpoint, expiry)) => {
42+
*self.endpoint.lock().unwrap() = Some(ExpiringEndpoint { endpoint, expiry })
43+
}
44+
Err(err) => *self.error.lock().unwrap() = Some(err),
45+
}
46+
}
47+
48+
/// An infinite loop task that will reload the endpoint
49+
///
50+
/// This task will terminate when the corresponding [`Client`](crate::Client) is dropped.
51+
pub async fn reload_task(mut self) {
52+
loop {
53+
match self.rx.try_recv() {
54+
Ok(_) | Err(TryRecvError::Closed) => break,
55+
_ => {}
56+
}
57+
self.reload_increment(self.time.now()).await;
58+
self.sleep.sleep(Duration::from_secs(60)).await;
59+
}
60+
}
61+
62+
async fn reload_increment(&self, now: SystemTime) {
63+
let should_reload = self
64+
.endpoint
65+
.lock()
66+
.unwrap()
67+
.as_ref()
68+
.map(|e| e.is_expired(now))
69+
.unwrap_or(true);
70+
if should_reload {
71+
tracing::debug!("reloading endpoint, previous endpoint was expired");
72+
self.reload_once().await;
73+
}
74+
}
75+
}
76+
77+
#[derive(Debug, Clone)]
78+
pub(crate) struct EndpointCache {
79+
error: Arc<Mutex<Option<ResolveEndpointError>>>,
80+
endpoint: Arc<Mutex<Option<ExpiringEndpoint>>>,
81+
// When the sender is dropped, this allows the reload loop to stop
82+
_drop_guard: Arc<Sender<()>>,
83+
}
84+
85+
impl<T> ResolveEndpoint<T> for EndpointCache {
86+
fn resolve_endpoint(&self, _params: &T) -> aws_smithy_http::endpoint::Result {
87+
self.resolve_endpoint()
88+
}
89+
}
90+
91+
#[derive(Debug)]
92+
struct ExpiringEndpoint {
93+
endpoint: Endpoint,
94+
expiry: SystemTime,
95+
}
96+
97+
impl ExpiringEndpoint {
98+
fn is_expired(&self, now: SystemTime) -> bool {
99+
tracing::debug!(expiry = ?self.expiry, now = ?now, delta = ?self.expiry.duration_since(now), "checking expiry status of endpoint");
100+
match self.expiry.duration_since(now) {
101+
Err(_) => true,
102+
Ok(t) => t < Duration::from_secs(120),
103+
}
104+
}
105+
}
106+
107+
pub(crate) async fn create_cache<F>(
108+
loader_fn: impl Fn() -> F + Send + Sync + 'static,
109+
sleep: Arc<dyn AsyncSleep>,
110+
time: Arc<dyn TimeSource>,
111+
) -> Result<(EndpointCache, ReloadEndpoint), ResolveEndpointError>
112+
where
113+
F: Future<Output = Result<(Endpoint, SystemTime), ResolveEndpointError>> + Send + 'static,
114+
{
115+
let error_holder = Arc::new(Mutex::new(None));
116+
let endpoint_holder = Arc::new(Mutex::new(None));
117+
let (tx, rx) = tokio::sync::oneshot::channel();
118+
let cache = EndpointCache {
119+
error: error_holder.clone(),
120+
endpoint: endpoint_holder.clone(),
121+
_drop_guard: Arc::new(tx),
122+
};
123+
let reloader = ReloadEndpoint {
124+
loader: Box::new(move || Box::pin((loader_fn)()) as _),
125+
endpoint: endpoint_holder,
126+
error: error_holder,
127+
rx,
128+
sleep,
129+
time,
130+
};
131+
reloader.reload_once().await;
132+
// if we didn't successfully get an endpoint, bail out so the client knows
133+
// configuration failed to work
134+
cache.resolve_endpoint()?;
135+
Ok((cache, reloader))
136+
}
137+
138+
impl EndpointCache {
139+
fn resolve_endpoint(&self) -> aws_smithy_http::endpoint::Result {
140+
self.endpoint
141+
.lock()
142+
.unwrap()
143+
.as_ref()
144+
.map(|e| e.endpoint.clone())
145+
.ok_or_else(|| {
146+
self.error
147+
.lock()
148+
.unwrap()
149+
.take()
150+
.unwrap_or_else(|| ResolveEndpointError::message("no endpoint loaded"))
151+
})
152+
}
153+
}
154+
155+
#[cfg(test)]
156+
mod test {
157+
use crate::endpoint_discovery::create_cache;
158+
use aws_smithy_async::rt::sleep::TokioSleep;
159+
use aws_smithy_async::test_util::controlled_time_and_sleep;
160+
use aws_smithy_async::time::SystemTimeSource;
161+
use aws_smithy_types::endpoint::Endpoint;
162+
use std::sync::atomic::{AtomicUsize, Ordering};
163+
use std::sync::Arc;
164+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
165+
use tokio::time::timeout;
166+
167+
fn check_send_v<T: Send>(t: T) -> T {
168+
t
169+
}
170+
171+
#[tokio::test]
172+
#[allow(unused_must_use)]
173+
async fn check_traits() {
174+
let (cache, reloader) = create_cache(
175+
|| async {
176+
Ok((
177+
Endpoint::builder().url("http://foo.com").build(),
178+
SystemTime::now(),
179+
))
180+
},
181+
Arc::new(TokioSleep::new()),
182+
Arc::new(SystemTimeSource::new()),
183+
)
184+
.await
185+
.unwrap();
186+
check_send_v(reloader.reload_task());
187+
check_send_v(cache);
188+
}
189+
190+
#[tokio::test]
191+
async fn erroring_endpoint_always_reloaded() {
192+
let expiry = UNIX_EPOCH + Duration::from_secs(123456789);
193+
let ct = Arc::new(AtomicUsize::new(0));
194+
let (cache, reloader) = create_cache(
195+
move || {
196+
let shared_ct = ct.clone();
197+
shared_ct.fetch_add(1, Ordering::AcqRel);
198+
async move {
199+
Ok((
200+
Endpoint::builder()
201+
.url(format!("http://foo.com/{shared_ct:?}"))
202+
.build(),
203+
expiry,
204+
))
205+
}
206+
},
207+
Arc::new(TokioSleep::new()),
208+
Arc::new(SystemTimeSource::new()),
209+
)
210+
.await
211+
.expect("returns an endpoint");
212+
assert_eq!(
213+
cache.resolve_endpoint().expect("ok").url(),
214+
"http://foo.com/1"
215+
);
216+
// 120 second buffer
217+
reloader
218+
.reload_increment(expiry - Duration::from_secs(240))
219+
.await;
220+
assert_eq!(
221+
cache.resolve_endpoint().expect("ok").url(),
222+
"http://foo.com/1"
223+
);
224+
225+
reloader.reload_increment(expiry).await;
226+
assert_eq!(
227+
cache.resolve_endpoint().expect("ok").url(),
228+
"http://foo.com/2"
229+
);
230+
}
231+
232+
#[tokio::test]
233+
async fn test_advance_of_task() {
234+
let expiry = UNIX_EPOCH + Duration::from_secs(123456789);
235+
// expires in 8 minutes
236+
let (time, sleep, mut gate) = controlled_time_and_sleep(expiry - Duration::from_secs(239));
237+
let ct = Arc::new(AtomicUsize::new(0));
238+
let (cache, reloader) = create_cache(
239+
move || {
240+
let shared_ct = ct.clone();
241+
shared_ct.fetch_add(1, Ordering::AcqRel);
242+
async move {
243+
Ok((
244+
Endpoint::builder()
245+
.url(format!("http://foo.com/{shared_ct:?}"))
246+
.build(),
247+
expiry,
248+
))
249+
}
250+
},
251+
Arc::new(sleep.clone()),
252+
Arc::new(time.clone()),
253+
)
254+
.await
255+
.expect("first load success");
256+
let reload_task = tokio::spawn(reloader.reload_task());
257+
assert!(!reload_task.is_finished());
258+
// expiry occurs after 2 sleeps
259+
// t = 0
260+
assert_eq!(
261+
gate.expect_sleep().await.duration(),
262+
Duration::from_secs(60)
263+
);
264+
assert_eq!(cache.resolve_endpoint().unwrap().url(), "http://foo.com/1");
265+
// t = 60
266+
267+
let sleep = gate.expect_sleep().await;
268+
// we're still holding the drop guard, so we haven't expired yet.
269+
assert_eq!(cache.resolve_endpoint().unwrap().url(), "http://foo.com/1");
270+
assert_eq!(sleep.duration(), Duration::from_secs(60));
271+
sleep.allow_progress();
272+
// t = 120
273+
274+
let sleep = gate.expect_sleep().await;
275+
assert_eq!(cache.resolve_endpoint().unwrap().url(), "http://foo.com/2");
276+
sleep.allow_progress();
277+
278+
let sleep = gate.expect_sleep().await;
279+
drop(cache);
280+
sleep.allow_progress();
281+
282+
timeout(Duration::from_secs(1), reload_task)
283+
.await
284+
.expect("task finishes successfully")
285+
.expect("finishes");
286+
}
287+
}

aws/rust-runtime/aws-inlineable/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,12 @@ pub mod route53_resource_id_preprocessor;
4545

4646
/// Convert a streaming `SdkBody` into an aws-chunked streaming body with checksum trailers
4747
pub mod http_body_checksum;
48+
49+
#[allow(dead_code)]
50+
pub mod endpoint_discovery;
51+
52+
// just so docs work
53+
#[allow(dead_code)]
54+
/// allow docs to work
55+
#[derive(Debug)]
56+
pub struct Client;

aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import software.amazon.smithy.rustsdk.customize.s3.S3ExtendedRequestIdDecorator
2121
import software.amazon.smithy.rustsdk.customize.s3control.S3ControlDecorator
2222
import software.amazon.smithy.rustsdk.customize.sso.SSODecorator
2323
import software.amazon.smithy.rustsdk.customize.sts.STSDecorator
24+
import software.amazon.smithy.rustsdk.customize.timestream.TimestreamDecorator
2425
import software.amazon.smithy.rustsdk.endpoints.AwsEndpointsStdLib
2526
import software.amazon.smithy.rustsdk.endpoints.OperationInputTestDecorator
2627
import software.amazon.smithy.rustsdk.endpoints.RequireEndpointRules
@@ -69,6 +70,8 @@ val DECORATORS: List<ClientCodegenDecorator> = listOf(
6970
S3ControlDecorator().onlyApplyTo("com.amazonaws.s3control#AWSS3ControlServiceV20180820"),
7071
STSDecorator().onlyApplyTo("com.amazonaws.sts#AWSSecurityTokenServiceV20110615"),
7172
SSODecorator().onlyApplyTo("com.amazonaws.sso#SWBPortalService"),
73+
TimestreamDecorator().onlyApplyTo("com.amazonaws.timestreamwrite#Timestream_20181101"),
74+
TimestreamDecorator().onlyApplyTo("com.amazonaws.timestreamquery#Timestream_20181101"),
7275

7376
// Only build docs-rs for linux to reduce load on docs.rs
7477
listOf(

aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCrateDocsDecorator.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rawTemplate
2121
import software.amazon.smithy.rust.codegen.core.rustlang.rust
2222
import software.amazon.smithy.rust.codegen.core.rustlang.writable
2323
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
24+
import software.amazon.smithy.rust.codegen.core.smithy.customize.AdHocSection
2425
import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsCustomization
2526
import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsSection
2627
import software.amazon.smithy.rust.codegen.core.smithy.generators.ManifestCustomizations
@@ -85,6 +86,10 @@ class AwsCrateDocsDecorator : ClientCodegenDecorator {
8586
SdkSettings.from(codegenContext.settings).generateReadme
8687
}
8788

89+
sealed class DocSection(name: String) : AdHocSection(name) {
90+
data class CreateClient(val crateName: String, val clientName: String = "client", val indent: String) : DocSection("CustomExample")
91+
}
92+
8893
internal class AwsCrateDocGenerator(private val codegenContext: ClientCodegenContext) {
8994
private val logger: Logger = Logger.getLogger(javaClass.name)
9095
private val awsConfigVersion by lazy {
@@ -154,8 +159,7 @@ internal class AwsCrateDocGenerator(private val codegenContext: ClientCodegenCon
154159
155160
##[#{tokio}::main]
156161
async fn main() -> Result<(), $shortModuleName::Error> {
157-
let config = #{aws_config}::load_from_env().await;
158-
let client = $shortModuleName::Client::new(&config);
162+
#{constructClient}
159163
160164
// ... make some calls with the client
161165
@@ -171,6 +175,7 @@ internal class AwsCrateDocGenerator(private val codegenContext: ClientCodegenCon
171175
true -> AwsCargoDependency.awsConfig(codegenContext.runtimeConfig).toDevDependency().toType()
172176
else -> writable { rust("aws_config") }
173177
},
178+
"constructClient" to AwsDocs.constructClient(codegenContext, indent = " "),
174179
)
175180

176181
template(

0 commit comments

Comments
 (0)