google_cloud_pubsub/subscriber/builder.rs
1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::MessageStream;
16use super::ShutdownBehavior;
17use super::transport::Transport;
18use std::sync::Arc;
19use std::time::Duration;
20
21const MIB: i64 = 1024 * 1024;
22// Clamp the max lease to 100 years to avoid overflow errors.
23const MAX_LEASE: Duration = Duration::from_secs(100 * 365 * 24 * 60 * 60);
24
25pub use super::client_builder::ClientBuilder;
26
27/// Builder for the [`client::Subscriber::subscribe`][crate::client::Subscriber::subscribe] method.
28pub struct Subscribe {
29 pub(super) inner: Arc<Transport>,
30 pub(super) subscription: String,
31 pub(super) client_id: String,
32 pub(super) grpc_subchannel_count: usize,
33 pub(super) ack_deadline_seconds: i32,
34 pub(super) max_lease: Duration,
35 pub(super) max_outstanding_messages: i64,
36 pub(super) max_outstanding_bytes: i64,
37 pub(super) shutdown_behavior: ShutdownBehavior,
38}
39
40impl Subscribe {
41 pub(super) fn new(
42 inner: Arc<Transport>,
43 subscription: String,
44 client_id: String,
45 grpc_subchannel_count: usize,
46 ) -> Self {
47 Self {
48 inner,
49 subscription,
50 client_id,
51 grpc_subchannel_count,
52 ack_deadline_seconds: 60,
53 max_lease: Duration::from_secs(60 * 60),
54 max_outstanding_messages: 1000,
55 max_outstanding_bytes: 100 * MIB,
56 shutdown_behavior: ShutdownBehavior::WaitForProcessing,
57 }
58 }
59
60 /// Creates a new stream to receive messages from the subscription.
61 ///
62 /// # Example
63 /// ```
64 /// # use google_cloud_pubsub::client::Subscriber;
65 /// # async fn sample(client: Subscriber) -> anyhow::Result<()> {
66 /// let mut stream = client
67 /// .subscribe("projects/my-project/subscriptions/my-subscription")
68 /// .build();
69 /// while let Some((m, h)) = stream.next().await.transpose()? {
70 /// println!("Received message m={m:?}");
71 /// h.ack();
72 /// }
73 /// # Ok(()) }
74 /// ```
75 ///
76 /// Note that the underlying connection with the server is lazy-initialized.
77 /// It is not established until [`MessageStream::next()`] is called.
78 pub fn build(self) -> MessageStream {
79 MessageStream::new(self)
80 }
81
82 /// Sets the maximum lease deadline for a message.
83 ///
84 /// # Example
85 /// ```
86 /// # use google_cloud_pubsub::client::Subscriber;
87 /// # use std::time::Duration;
88 /// # async fn sample() -> anyhow::Result<()> {
89 /// # let client = Subscriber::builder().build().await?;
90 /// let stream = client.subscribe("projects/my-project/subscriptions/my-subscription")
91 /// .set_max_lease(Duration::from_secs(3600))
92 /// .build();
93 /// # Ok(()) }
94 /// ```
95 ///
96 /// The client holds a message for at most this amount. After a message has
97 /// been held for this long, the client will stop extending its lease.
98 ///
99 /// The default value is 60 minutes. If it takes your application longer
100 /// than 60 minutes to process a message, you should increase this value.
101 pub fn set_max_lease<T: Into<Duration>>(mut self, v: T) -> Self {
102 self.max_lease = v.into().min(MAX_LEASE);
103 self
104 }
105
106 /// Sets the maximum duration to extend lease deadlines by.
107 ///
108 /// # Example
109 /// ```
110 /// # use google_cloud_pubsub::client::Subscriber;
111 /// # use std::time::Duration;
112 /// # async fn sample() -> anyhow::Result<()> {
113 /// # let client = Subscriber::builder().build().await?;
114 /// let stream = client.subscribe("projects/my-project/subscriptions/my-subscription")
115 /// .set_max_lease_extension(Duration::from_secs(20))
116 /// .build();
117 /// # Ok(()) }
118 /// ```
119 ///
120 /// The client extends lease deadlines by at most this amount.
121 ///
122 /// If the server does not hear back from the client within this deadline
123 /// (e.g. if an application crashes), it will resend any unacknowledged
124 /// messages to another subscriber.
125 ///
126 /// Note that this value is independent of the ack deadline configured on
127 /// the subscription.
128 ///
129 /// The minimum deadline you can specify is 10 seconds. The maximum deadline
130 /// you can specify is 10 minutes. The client clamps the supplied value to
131 /// this range.
132 ///
133 /// The default value is 60 seconds.
134 pub fn set_max_lease_extension<T: Into<Duration>>(mut self, v: T) -> Self {
135 self.ack_deadline_seconds = v.into().as_secs().clamp(10, 600) as i32;
136 self
137 }
138
139 /// Flow control settings for the maximum number of outstanding messages.
140 ///
141 /// # Example
142 /// ```
143 /// # use google_cloud_pubsub::client::Subscriber;
144 /// # async fn sample() -> anyhow::Result<()> {
145 /// # let client = Subscriber::builder().build().await?;
146 /// let stream = client.subscribe("projects/my-project/subscriptions/my-subscription")
147 /// .set_max_outstanding_messages(2000)
148 /// .build();
149 /// # Ok(()) }
150 /// ```
151 ///
152 /// The server will stop sending messages to a client when this many
153 /// messages are outstanding (i.e. that have not been acked). The server
154 /// resumes sending messages when the outstanding message count drops below
155 /// this value.
156 ///
157 /// The limit applies per-stream. It is not a global limit.
158 ///
159 /// Use a value <= 0 to set no limit on the number of outstanding messages.
160 ///
161 /// The default value is 1000 messages.
162 pub fn set_max_outstanding_messages<T: Into<i64>>(mut self, v: T) -> Self {
163 self.max_outstanding_messages = v.into();
164 self
165 }
166
167 /// Flow control settings for the maximum number of outstanding bytes.
168 ///
169 /// # Example
170 /// ```
171 /// # use google_cloud_pubsub::client::Subscriber;
172 /// # async fn sample() -> anyhow::Result<()> {
173 /// # let client = Subscriber::builder().build().await?;
174 /// const MIB: i64 = 1024 * 1024;
175 /// let stream = client.subscribe("projects/my-project/subscriptions/my-subscription")
176 /// .set_max_outstanding_bytes(200 * MIB)
177 /// .build();
178 /// # Ok(()) }
179 /// ```
180 ///
181 /// The server will stop sending messages to a client when this many bytes
182 /// of messages are outstanding (i.e. that have not been acked). The server
183 /// resumes sending messages when the outstanding byte count drops below
184 /// this value.
185 ///
186 /// The limit applies per-stream. It is not a global limit.
187 ///
188 /// Use a value <= 0 to set no limit on the number of outstanding bytes.
189 ///
190 /// The default value is 100 MiB.
191 pub fn set_max_outstanding_bytes<T: Into<i64>>(mut self, v: T) -> Self {
192 self.max_outstanding_bytes = v.into();
193 self
194 }
195
196 /// Sets the shutdown behavior for the stream.
197 ///
198 /// # Example
199 /// ```
200 /// # use google_cloud_pubsub::client::Subscriber;
201 /// # async fn sample() -> anyhow::Result<()> {
202 /// # let client = Subscriber::builder().build().await?;
203 /// use google_cloud_pubsub::subscriber::ShutdownBehavior::NackImmediately;
204 /// let stream = client.subscribe("projects/my-project/subscriptions/my-subscription")
205 /// .set_shutdown_behavior(NackImmediately)
206 /// .build();
207 /// # Ok(()) }
208 /// ```
209 ///
210 /// The default behavior is [`WaitForProcessing`][wait].
211 ///
212 /// [wait]: crate::subscriber::ShutdownBehavior::WaitForProcessing
213 pub fn set_shutdown_behavior(mut self, v: ShutdownBehavior) -> Self {
214 self.shutdown_behavior = v;
215 self
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use gaxi::options::ClientConfig;
223 use google_cloud_auth::credentials::anonymous::Builder as Anonymous;
224 use test_case::test_case;
225
226 const KIB: i64 = 1024;
227
228 async fn test_inner() -> anyhow::Result<Arc<Transport>> {
229 let mut config = ClientConfig::default();
230 config.cred = Some(Anonymous::new().build());
231 let transport = Transport::new(config).await?;
232 Ok(Arc::new(transport))
233 }
234
235 #[tokio::test]
236 async fn reasonable_defaults() -> anyhow::Result<()> {
237 let builder = Subscribe::new(
238 test_inner().await?,
239 "projects/my-project/subscriptions/my-subscription".to_string(),
240 "client-id".to_string(),
241 1_usize,
242 );
243 assert_eq!(
244 builder.subscription,
245 "projects/my-project/subscriptions/my-subscription"
246 );
247 assert_eq!(builder.grpc_subchannel_count, 1);
248 assert_eq!(builder.ack_deadline_seconds, 60);
249 assert!(
250 builder.max_lease >= Duration::from_secs(300),
251 "max_lease={:?}",
252 builder.max_lease
253 );
254 assert!(
255 100_000 > builder.max_outstanding_messages && builder.max_outstanding_messages > 100,
256 "max_outstanding_messages={}",
257 builder.max_outstanding_messages
258 );
259 assert!(
260 builder.max_outstanding_bytes > 100 * KIB,
261 "max_outstanding_bytes={}",
262 builder.max_outstanding_bytes
263 );
264 assert_eq!(
265 builder.shutdown_behavior,
266 ShutdownBehavior::WaitForProcessing
267 );
268
269 Ok(())
270 }
271
272 #[tokio::test]
273 async fn options() -> anyhow::Result<()> {
274 let builder = Subscribe::new(
275 test_inner().await?,
276 "projects/my-project/subscriptions/my-subscription".to_string(),
277 "client-id".to_string(),
278 2_usize,
279 )
280 .set_max_lease(Duration::from_secs(3600))
281 .set_max_lease_extension(Duration::from_secs(20))
282 .set_max_outstanding_messages(12345)
283 .set_max_outstanding_bytes(6789 * KIB)
284 .set_shutdown_behavior(ShutdownBehavior::NackImmediately);
285 assert_eq!(
286 builder.subscription,
287 "projects/my-project/subscriptions/my-subscription"
288 );
289 assert_eq!(builder.grpc_subchannel_count, 2);
290 assert_eq!(builder.max_lease, Duration::from_secs(3600));
291 assert_eq!(builder.ack_deadline_seconds, 20);
292 assert_eq!(builder.max_outstanding_messages, 12345);
293 assert_eq!(builder.max_outstanding_bytes, 6789 * KIB);
294 assert_eq!(builder.shutdown_behavior, ShutdownBehavior::NackImmediately);
295
296 Ok(())
297 }
298
299 #[test_case(Duration::ZERO, 10)]
300 #[test_case(Duration::from_secs(42), 42)]
301 #[test_case(Duration::from_secs(4200), 600)]
302 #[tokio::test]
303 async fn clamp_ack_deadline(v: Duration, want: i32) -> anyhow::Result<()> {
304 let builder = Subscribe::new(
305 test_inner().await?,
306 "projects/my-project/subscriptions/my-subscription".to_string(),
307 "client-id".to_string(),
308 1_usize,
309 )
310 .set_max_lease_extension(v);
311 assert_eq!(builder.ack_deadline_seconds, want);
312
313 Ok(())
314 }
315
316 #[tokio::test]
317 async fn clamp_max_lease() -> anyhow::Result<()> {
318 let builder = Subscribe::new(
319 test_inner().await?,
320 "projects/my-project/subscriptions/my-subscription".to_string(),
321 "client-id".to_string(),
322 1_usize,
323 )
324 .set_max_lease(Duration::MAX);
325 assert_eq!(builder.max_lease, MAX_LEASE);
326
327 Ok(())
328 }
329}