azure_iot_operations_services/leased_lock/lease.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Client for Lease operations.
use std::{sync::Arc, sync::Mutex, time::Duration};
use tokio::{select, sync::Notify};
use crate::leased_lock::{Error, ErrorKind, LeaseObservation, SetCondition, SetOptions};
use crate::state_store;
use azure_iot_operations_mqtt::interface::ManagedClient;
use azure_iot_operations_protocol::common::hybrid_logical_clock::HybridLogicalClock;
/// Lease client struct.
#[derive(Clone)]
pub struct Client<C>
where
C: ManagedClient + Clone + Send + Sync + 'static,
C::PubReceiver: Send + Sync,
{
state_store: Arc<state_store::Client<C>>,
lease_name: Vec<u8>,
lease_holder_name: Vec<u8>,
current_fencing_token: Arc<Mutex<Option<HybridLogicalClock>>>,
auto_renewal_notify: Arc<Notify>,
}
/// Lease client implementation
///
/// Notes:
/// Do not call any of the methods of this client after the `state_store` parameter is shutdown.
/// Calling any of the methods in this implementation after the `state_store` is shutdown results in undefined behavior.
impl<C> Client<C>
where
C: ManagedClient + Clone + Send + Sync,
C::PubReceiver: Send + Sync,
{
/// Create a new Lease Client.
///
/// Notes:
/// - `lease_holder_name` is expected to be the client ID used in the underlying MQTT connection settings.
/// - There must be only one instance of `lease::Client` per lease.
///
/// # Errors
/// [`struct@Error`] of kind [`InvalidArgument`](ErrorKind::InvalidArgument) if the either `lease_name` or `lease_holder_name` is empty.
pub fn new(
state_store: Arc<state_store::Client<C>>,
lease_name: Vec<u8>,
lease_holder_name: Vec<u8>,
) -> Result<Self, Error> {
if lease_name.is_empty() {
return Err(Error(ErrorKind::InvalidArgument(
"lease_name is empty".to_string(),
)));
}
if lease_holder_name.is_empty() {
return Err(Error(ErrorKind::InvalidArgument(
"lease_holder_name is empty".to_string(),
)));
}
Ok(Self {
state_store,
lease_name,
lease_holder_name,
current_fencing_token: Arc::new(Mutex::new(None)),
auto_renewal_notify: Arc::new(Notify::new()),
})
}
/// Gets the latest fencing token related to the most recent lease.
///
/// Returns either None or an actual Fencing Token (`HybridLogicalClock`).
/// None means that either a lease has not been acquired previously with this client, or
/// a lease renewal has failed (if lease auto-renewal is used). The presence of a `HybridLogicalClock`
/// does not mean that it is the most recent (and thus valid) Fencing Token - this can
/// happen in the scenario where auto-renewal has not been used and the lease has already expired.
///
/// # Panics
/// If the lock on the `current_fencing_token` is poisoned, which should not be possible.
#[must_use]
pub fn current_lease_fencing_token(&self) -> Option<HybridLogicalClock> {
self.current_fencing_token.lock().unwrap().clone()
}
async fn internal_acquire(
&self,
lease_expiration: Duration,
request_timeout: Duration,
) -> Result<HybridLogicalClock, Error> {
let state_store_response = self
.state_store
.set(
self.lease_name.clone(),
self.lease_holder_name.clone(),
request_timeout,
None,
SetOptions {
set_condition: SetCondition::OnlyIfEqualOrDoesNotExist,
expires: Some(lease_expiration),
},
)
.await?;
if state_store_response.response {
self.current_fencing_token
.lock()
.unwrap()
.clone_from(&state_store_response.version);
state_store_response
.version
.ok_or(Error(ErrorKind::MissingFencingToken))
} else {
*self.current_fencing_token.lock().unwrap() = None;
Err(Error(ErrorKind::LeaseAlreadyHeld))
}
}
/// Attempts to acquire a lease, returning if it cannot be acquired after one attempt.
///
/// `lease_expiration` is how long the lease will remain held in the State Store after acquired, if not released before then.
/// `request_timeout` is the maximum time the function will wait for receiving a response from the State Store service, it is rounded up to the nearest second.
/// `renewal_period` is the frequency with which the lease will be auto-renewed by the lease client if acquired successfully. `None` (or zero) indicates the lease should not be auto-renewed.
///
/// Note:
/// If lease auto-renewal is used when acquiring a lease, an auto-renewal task is spawned.
/// To terminate this task and stop the lease auto-renewal, `lease::Client::release()` must be called.
/// Simply dropping the `lease::Client` instance will not terminate the auto-renewal task.
/// This logic is intended for a scenario where the `lease::Client` is cloned and a lease is acquired with auto-renewal by the original instance.
/// If the original instance is dropped, its clone remains in control of the lease (through the auto-renewal task that remains active).
/// Special attention must be used to avoid a memory leak if `lease::Client::release()` is never called in this scenario.
///
/// Returns Ok with a fencing token (`HybridLogicalClock`) if completed successfully, or `Error` if lease is not acquired.
/// # Errors
/// [`struct@Error`] of kind [`InvalidArgument`](ErrorKind::InvalidArgument) if the `request_timeout` is zero or > `u32::max`
///
/// [`struct@Error`] of kind [`ServiceError`](ErrorKind::ServiceError) if the State Store returns an Error response
///
/// [`struct@Error`] of kind [`UnexpectedPayload`](ErrorKind::UnexpectedPayload) if the State Store returns a response that isn't valid for a `Set` request
///
/// [`struct@Error`] of kind [`AIOProtocolError`](ErrorKind::AIOProtocolError) if there are any underlying errors from the command invoker
///
/// [`struct@Error`] of kind [`LeaseAlreadyHeld`](ErrorKind::LeaseAlreadyHeld) if the `lease` is already in use by another holder
///
/// [`struct@Error`] of kind [`MissingFencingToken`](ErrorKind::MissingFencingToken) if the fencing token in the service response is empty.
pub async fn acquire(
&self,
lease_expiration: Duration,
request_timeout: Duration,
renewal_period: Option<Duration>,
) -> Result<HybridLogicalClock, Error> {
if renewal_period.is_some_and(|rp| rp >= lease_expiration) {
return Err(Error(ErrorKind::InvalidArgument(
"renewal_period must be less than lease_expiration".to_string(),
)));
}
// Stop auto-renewal.
self.auto_renewal_notify.notify_waiters();
let acquire_result = self
.internal_acquire(lease_expiration, request_timeout)
.await;
if let Some(renewal_period) = renewal_period {
if renewal_period > Duration::ZERO {
let self_clone = self.clone();
tokio::task::spawn({
async move {
loop {
select! {
() = self_clone.auto_renewal_notify.notified() => {
break; // Auto-renewal is cancelled.
}
() = tokio::time::sleep(renewal_period) => {
if self_clone
.internal_acquire(lease_expiration, request_timeout)
.await
.is_err()
{
// Acquire failed. Stopping Auto-renewal.
break;
}
}
}
}
}
});
}
}
acquire_result
}
/// Releases a lease if and only if requested by the lease holder (same client id).
///
/// Note: `request_timeout` is rounded up to the nearest second.
///
/// Returns `Ok()` if lease is no longer held by this `lease_holder`, or `Error` otherwise.
///
/// Even if this method fails the current fencing token (obtained by calling `current_lease_fencing_token()`) is cleared
/// and the auto-renewal task is terminated (if the lease was acquired using auto-renewal).
///
/// # Errors
/// [`struct@Error`] of kind [`InvalidArgument`](ErrorKind::InvalidArgument) if the `request_timeout` is zero or > `u32::max`
///
/// [`struct@Error`] of kind [`ServiceError`](ErrorKind::ServiceError) if the State Store returns an Error response
///
/// [`struct@Error`] of kind [`UnexpectedPayload`](ErrorKind::UnexpectedPayload) if the State Store returns a response that isn't valid for a `V Delete` request
///
/// [`struct@Error`] of kind [`AIOProtocolError`](ErrorKind::AIOProtocolError) if there are any underlying errors from the command invoker
///
/// # Panics
/// If the lock on the `current_fencing_token` is poisoned, which should not be possible.
pub async fn release(&self, request_timeout: Duration) -> Result<(), Error> {
// Stop auto-renewal.
self.auto_renewal_notify.notify_waiters();
*self.current_fencing_token.lock().unwrap() = None;
self.state_store
.vdel(
self.lease_name.clone(),
self.lease_holder_name.clone(),
None,
request_timeout,
)
.await?;
Ok(())
}
/// Starts observation of any changes on a lease
///
/// Note: `request_timeout` is rounded up to the nearest second.
///
/// Returns OK([`LeaseObservation`]) if the lease is now being observed.
/// The [`LeaseObservation`] can be used to receive lease notifications for this lease
///
/// <div class="warning">
///
/// If a client disconnects, `observe` must be called again by the user.
///
/// </div>
///
/// # Errors
/// [`struct@Error`] of kind [`InvalidArgument`](ErrorKind::InvalidArgument) if
/// - the `request_timeout` is zero or > `u32::max`
///
/// [`struct@Error`] of kind [`ServiceError`](ErrorKind::ServiceError) if
/// - the State Store returns an Error response
/// - the State Store returns a response that isn't valid for an `Observe` request
///
/// [`struct@Error`] of kind [`AIOProtocolError`](ErrorKind::AIOProtocolError) if
/// - there are any underlying errors from the command invoker
pub async fn observe(&self, request_timeout: Duration) -> Result<LeaseObservation, Error> {
Ok(self
.state_store
.observe(self.lease_name.clone(), request_timeout)
.await?
.response)
}
/// Stops observation of any changes on a lease.
///
/// Note: `request_timeout` is rounded up to the nearest second.
///
/// Returns `true` if the lease is no longer being observed or `false` if the lease wasn't being observed
/// # Errors
/// [`struct@Error`] of kind [`InvalidArgument`](ErrorKind::InvalidArgument) if
/// - the `request_timeout` is zero or > `u32::max`
///
/// [`struct@Error`] of kind [`ServiceError`](ErrorKind::ServiceError) if
/// - the State Store returns an Error response
/// - the State Store returns a response that isn't valid for an `Unobserve` request
///
/// [`struct@Error`] of kind [`AIOProtocolError`](ErrorKind::AIOProtocolError) if
/// - there are any underlying errors from the command invoker
pub async fn unobserve(&self, request_timeout: Duration) -> Result<bool, Error> {
Ok(self
.state_store
.unobserve(self.lease_name.clone(), request_timeout)
.await?
.response)
}
/// Gets the name of the holder of a lease
///
/// Note: `request_timeout` is rounded up to the nearest second.
///
/// Returns `Some(<holder of the lease>)` if the lease is found or `None`
/// if the lease was not found (i.e., was not acquired by anyone, already released or expired).
///
/// # Errors
/// [`struct@Error`] of kind [`InvalidArgument`](ErrorKind::InvalidArgument) if the `request_timeout` is zero or > `u32::max`
///
/// [`struct@Error`] of kind [`ServiceError`](ErrorKind::ServiceError) if the State Store returns an Error response
///
/// [`struct@Error`] of kind [`UnexpectedPayload`](ErrorKind::UnexpectedPayload) if the State Store returns a response that isn't valid for a `Get` request
///
/// [`struct@Error`] of kind [`AIOProtocolError`](ErrorKind::AIOProtocolError) if there are any underlying errors from the command invoker
pub async fn get_holder(&self, request_timeout: Duration) -> Result<Option<Vec<u8>>, Error> {
Ok(self
.state_store
.get(self.lease_name.clone(), request_timeout)
.await?
.response)
}
}