pdk_contracts_lib/legacy/api/
validator.rs1#![allow(deprecated)]
6
7use crate::api::credentials::{ClientId, ClientSecret};
8use crate::api::error::UpdateError;
9use crate::api::error::{AuthenticationError, AuthorizationError};
10use crate::api::ClientData;
11use crate::legacy::api::authentication::authenticate;
12use crate::legacy::api::authorization::authorize;
13use crate::legacy::api::tracker::AttemptTracker;
14use crate::legacy::implementation::constants::{
15 ACCEPT_HASH_ALGORITHM_VALUE, CONTRACTS_CACHE_EXPIRATION, CONTRACTS_FETCHING_INTERVAL,
16 LOCK_TIMEOUT_DURATION, PRIMARY_BACKUP_INTERVAL, PRIMARY_INACTIVE_TIMEOUT,
17 RETRY_GUARD_DEFAULT_BACKOFF, RETRY_GUARD_MAX_BACKOFF,
18};
19use crate::legacy::implementation::model::contracts_storage::{
20 ContractsLocalStorage, ContractsStorage,
21};
22use crate::legacy::implementation::model::distributed_storage::ContractsCache;
23use crate::legacy::implementation::model::session_storage::{
24 SessionSharedDataStorage, SessionStorage,
25};
26use crate::legacy::implementation::platform::client::HttpPlatformClient;
27use crate::legacy::implementation::platform::responses::{
28 contract_from_event, ContractsResponse, LoginResponse,
29};
30use crate::legacy::implementation::platform::shared::{AccessToken, ContractsRequestParams};
31use data_storage_lib::DataStorageBuilder;
32use lock_lib::{Lock, LockBuilder, TryLock};
33use pdk_core::classy::extract::context::ConfigureContext;
34use pdk_core::classy::extract::{Extract, FromContext};
35use pdk_core::classy::hl::HttpClientError;
36use pdk_core::classy::proxy_wasm::types::Status;
37use pdk_core::classy::{Clock, SharedData};
38use pdk_core::logger;
39use pdk_core::logger::debug;
40use pdk_core::policy_context::api::Metadata;
41use std::rc::Rc;
42use std::time::Duration;
43use thiserror::Error;
44
45pub struct ContractValidator {
54 api_id: String,
55 client: HttpPlatformClient,
56 session_lock: TryLock,
57 api_lock: TryLock,
58 session_storage: SessionSharedDataStorage,
59 contract_storage: Rc<ContractsLocalStorage>,
60 contracts_cache: ContractsCache,
61 clock: Rc<dyn Clock>,
62 tracker: AttemptTracker,
63}
64
65#[derive(Error, Debug)]
66enum InternalUpdateError {
67 #[error("Http client error: {0}")]
68 HttpClientError(#[from] HttpClientError),
69 #[error("Parsing error: {0}")]
70 Serde(#[from] serde_json::error::Error),
71 #[error("Lost the lock while executing async function.")]
72 LostLock,
73 #[error("Upstream returned unexpected status code {0}")]
74 UnexpectedResponse(u32),
75}
76
77enum PollContractsResponse {
78 Continue,
79 Renegotiate,
80 Finish,
81}
82
83pub(crate) enum PollerType {
84 Primary,
85 Secondary,
86}
87
88pub(crate) enum PollerError {
89 LostLock,
90 DataStorageError,
91}
92
93impl ContractValidator {
94 fn new(
95 client: HttpPlatformClient,
96 api_id: String,
97 clock: Rc<dyn Clock>,
98 shared_data: Rc<dyn SharedData>,
99 lock_builder: LockBuilder,
100 data_storage_builder: DataStorageBuilder,
101 ) -> Self {
102 let session_storage =
105 SessionSharedDataStorage::new(Rc::clone(&clock), Rc::clone(&shared_data));
106
107 let session_lock = lock_builder
108 .new(session_storage.session_lock_key())
109 .expiration(LOCK_TIMEOUT_DURATION)
110 .shared()
111 .build();
112
113 let contract_storage = Rc::new(ContractsLocalStorage::new(
114 &api_id,
115 Rc::clone(&clock),
116 shared_data,
117 ));
118 let api_lock = lock_builder
119 .new(contract_storage.api_lock_key())
120 .expiration(LOCK_TIMEOUT_DURATION)
121 .shared()
122 .build();
123
124 let contracts_cache = ContractsCache::new(
125 api_id.clone(),
126 Rc::clone(&clock),
127 data_storage_builder
128 .shared()
129 .remote("CONTRACTS-V2", CONTRACTS_CACHE_EXPIRATION),
130 Rc::clone(&contract_storage),
131 );
132
133 Self {
134 api_id,
135 client,
136 session_lock,
137 api_lock,
138 session_storage,
139 contract_storage,
140 clock: Rc::clone(&clock),
141 contracts_cache,
142 tracker: AttemptTracker::new(clock, CONTRACTS_FETCHING_INTERVAL),
143 }
144 }
145
146 pub const INITIALIZATION_PERIOD: Duration = Duration::from_millis(100);
148
149 pub const UPDATE_PERIOD: Duration = CONTRACTS_FETCHING_INTERVAL;
151
152 pub fn authorize(&self, client_id: &ClientId) -> Result<ClientData, AuthorizationError> {
156 authorize(self.contract_storage.as_ref(), client_id)
157 }
158
159 pub fn authenticate(
164 &self,
165 client_id: &ClientId,
166 client_secret: &ClientSecret,
167 ) -> Result<ClientData, AuthenticationError> {
168 authenticate(self.contract_storage.as_ref(), client_id, client_secret)
169 }
170
171 pub fn is_ready(&self) -> bool {
175 self.contract_storage.last_update().is_some()
176 }
177
178 pub async fn update_contracts(&self) -> Result<(), UpdateError> {
186 if self.tracker.expired() {
190 self.tracker.track();
191 } else {
192 return Ok(());
193 }
194
195 if !self.should_update() {
196 debug!("Contracts update skipped since update period hasn't elapsed.");
197 return Ok(());
198 }
199
200 debug!("Fetching contracts for API {}", self.api_id);
201 let Some(api_lock) = self.api_lock.try_lock() else {
202 debug!(
203 "Other worker has the lock for API {}. Skipping update.",
204 self.api_id
205 );
206 return Ok(());
207 };
208
209 if self.first_cycle() {
210 let _ = self.cache_contracts_poll(&api_lock).await;
211 if !api_lock.refresh_lock() {
212 return Ok(());
213 }
214 }
215
216 if !self.should_update() {
218 debug!("Contracts update skipped since update period hasn't elapsed.");
219 return Ok(());
220 }
221
222 if let Some(until) = self.contract_storage.retry_after() {
228 if self.clock.get_current_time() < until {
229 debug!(
230 "Contracts update skipped for API {}; retry guard active.",
231 self.api_id
232 );
233 return Ok(());
234 }
235 }
236
237 let result = self.platform_contracts_poll(&api_lock).await;
238
239 if self.contract_storage.last_update().is_none() {
240 debug!("No successfully poll registered will not try to backup contracts");
241 return result.map(|_| ());
242 }
243
244 match self.poller_type(&api_lock).await {
245 Ok(PollerType::Primary) => {
246 self.backup_contracts(&api_lock, result.as_ref().map(|r| *r).unwrap_or_default())
247 .await;
248 }
249 Ok(PollerType::Secondary) => {
250 debug!("No update backup since we are a secondary node.");
251 }
252 Err(PollerError::LostLock) => {
253 debug!("Lost the api_lock while trying to become primary, skipping update.");
254 }
255 Err(PollerError::DataStorageError) => {
256 debug!("Unexpected error communicating with the data storage.");
257 }
258 };
259
260 result.map(|_| ())
261 }
262
263 async fn platform_contracts_poll(&self, api_lock: &'_ Lock<'_>) -> Result<bool, UpdateError> {
264 let mut updates = false;
265 debug!(
266 "Fetching contracts for API {} from contracts service",
267 self.api_id
268 );
269
270 let Some(token) = self.session_token().await else {
271 return Ok(updates);
272 };
273
274 if !api_lock.refresh_lock() {
275 debug!("Lost the api lock while fetching session token.");
276 return Ok(updates);
277 }
278
279 let mut token_data = token;
280 loop {
281 match self.poll_contracts(&token_data, api_lock).await {
282 Ok(PollContractsResponse::Continue) => {
283 debug!("Contract polling request successful. Chaining next request");
284 updates = true;
285 }
286 Ok(PollContractsResponse::Renegotiate) => {
287 let Some(token) = self.renegotiate_token(token_data).await else {
288 return Ok(updates);
290 };
291 token_data = token;
292 if !api_lock.refresh_lock() {
293 debug!("Lost the api lock while refreshing the session lock.");
294 return Ok(updates);
295 }
296 }
297 Ok(PollContractsResponse::Finish) => {
298 return Ok(updates);
299 }
300 Err(error) => {
301 debug!("Error while polling contracts: {error}");
302 return Ok(updates);
303 }
304 }
305 }
306 }
307
308 async fn cache_contracts_poll(&self, api_lock: &'_ Lock<'_>) -> Result<(), UpdateError> {
309 debug!("Fetching contracts for API {} from cache.", self.api_id);
310
311 let result = self.contracts_cache.get_state().await;
312
313 if !api_lock.refresh_lock() {
314 debug!("Lost the api lock while recovering state from remote storage.");
315 return Ok(());
316 }
317
318 result
319 .into_iter()
320 .for_each(|state| self.contract_storage.set_state(state));
321
322 Ok(())
323 }
324
325 async fn poller_type(&self, api_lock: &'_ Lock<'_>) -> Result<PollerType, PollerError> {
326 let Some(primary) = self.contract_storage.is_primary() else {
327 debug!("No information regarding primary node.");
328 return self.contracts_cache.try_primary(api_lock).await;
329 };
330
331 let primary_expired = self
332 .contract_storage
333 .last_primary_update()
334 .map(|last| last + PRIMARY_INACTIVE_TIMEOUT < self.clock.get_current_time())
335 .unwrap_or(true);
336
337 if primary_expired && !primary {
338 debug!("Secondary node trying to become primary due to timeout.");
339 return self.contracts_cache.try_primary(api_lock).await;
340 } else if primary_expired {
341 debug!("We lost the primary status. We'll become secondary for at least one polling cycle.");
342 self.contract_storage.set_primary(false);
343 return Ok(PollerType::Secondary);
344 }
345
346 match primary {
347 true => Ok(PollerType::Primary),
348 false => Ok(PollerType::Secondary),
349 }
350 }
351
352 fn first_cycle(&self) -> bool {
353 self.contract_storage.last_update().is_none()
354 }
355
356 fn should_update(&self) -> bool {
357 self.contract_storage
358 .last_update()
359 .map(|last| last + CONTRACTS_FETCHING_INTERVAL < self.clock.get_current_time())
360 .unwrap_or(true)
361 }
362
363 async fn backup_contracts(&self, api_lock: &'_ Lock<'_>, has_updates: bool) {
364 if !self.should_update_backup(has_updates) {
365 return;
366 }
367
368 let time = self.clock.get_current_time();
369 let mut update = self.contract_storage.get_state();
370 update.update_primary(time);
371 if self.contracts_cache.save_state(update).await {
372 self.contract_storage.set_primary_update(time);
373 }
374
375 if !api_lock.refresh_lock() {
376 debug!("Lost the api lock while backing data to cache.");
377 }
378 }
379
380 fn should_update_backup(&self, has_updates: bool) -> bool {
381 let Some(last_update) = self.contract_storage.last_update() else {
382 debug!("Skipping cache backup since no data to backup.");
383 return false;
384 };
385
386 if has_updates {
387 debug!("Will backup contracts since new updates are available.");
388 return true;
389 }
390
391 let Some(last_primary_update) = self.contract_storage.last_primary_update() else {
392 debug!("No local records of a primary node.");
393 return true;
394 };
395
396 if last_update < last_primary_update {
397 debug!("Skipping cache backup since no updates since last save.");
398 return false; }
400
401 let result = last_primary_update + PRIMARY_BACKUP_INTERVAL < self.clock.get_current_time();
402 if !result {
403 debug!("Skipping cache backup since the elapsed time is less than the refresh rate.");
404 }
405 result
406 }
407
408 async fn session_token(&self) -> Option<AccessToken> {
409 match self.session_storage.get_token() {
410 Some(token) => Some(token),
411 None => {
412 let Some(session_lock) = self.session_lock.try_lock() else {
413 debug!("Other worker has the session lock. Skipping update.");
414 return None;
415 };
416
417 if let Some(token) = self.session_storage.get_token() {
420 return Some(token);
421 }
422
423 self.fetch_session_token(&session_lock).await
424 }
425 }
426 }
427
428 async fn renegotiate_token(&self, old_token: AccessToken) -> Option<AccessToken> {
429 if let Some(token) = self.session_storage.get_token() {
431 if token != old_token {
432 return Some(token);
433 }
434 };
435
436 let Some(session_lock) = self.session_lock.try_lock() else {
438 debug!("Other worker has the session lock. Aborting token renegotiation");
439 return None;
440 };
441
442 if let Some(token) = self.session_storage.get_token() {
445 if token != old_token {
446 return Some(token);
447 }
448 };
449
450 self.fetch_session_token(&session_lock).await
451 }
452
453 async fn fetch_session_token(&self, session_lock: &'_ Lock<'_>) -> Option<AccessToken> {
454 match self.perform_login_request().await {
455 Ok(login) => {
456 if !session_lock.refresh_lock() {
457 debug!("Lost the session lock. Aborting update.");
458 return None;
459 }
460 let token = login.get_token();
461 let token_data = AccessToken::new(token.to_string(), login.get_type().to_string());
462 debug!("Obtained the session token.");
463 self.session_storage.save_token(token_data.clone());
464 Some(token_data)
465 }
466 Err(e) => {
467 self.set_retry_backoff(RETRY_GUARD_DEFAULT_BACKOFF);
468 logger::warn!(
469 "Unexpected error while performing login request {e}. Skipping update."
470 );
471 None
472 }
473 }
474 }
475
476 async fn perform_login_request(&self) -> Result<LoginResponse, InternalUpdateError> {
477 debug!("Getting platform token...");
478 match self.client.login().await? {
479 r if r.status_code() == 200 => Ok(serde_json::from_slice::<LoginResponse>(r.body())?),
480 r => {
481 debug!(
482 "Fetching contracts failed with status code: {} and body:\n {}",
483 r.status_code(),
484 String::from_utf8_lossy(r.body())
485 );
486 Err(InternalUpdateError::UnexpectedResponse(r.status_code()))
487 }
488 }
489 }
490
491 async fn poll_contracts(
492 &self,
493 access_token: &AccessToken,
494 api_lock: &'_ Lock<'_>,
495 ) -> Result<PollContractsResponse, InternalUpdateError> {
496 let token = access_token.get_access_token();
497 let response = match self
498 .client
499 .contracts(
500 token,
501 self.api_id.as_str(),
502 ACCEPT_HASH_ALGORITHM_VALUE,
503 self.next_url(),
504 )
505 .await
506 {
507 Ok(response) => response,
508 Err(error) => {
509 self.set_retry_backoff(RETRY_GUARD_DEFAULT_BACKOFF);
510 return Err(error.into());
511 }
512 };
513
514 if !api_lock.refresh_lock() {
515 return Err(InternalUpdateError::LostLock);
516 }
517
518 match response.status_code() {
519 200 => {
520 let contracts: ContractsResponse = match serde_json::from_slice(response.body()) {
521 Ok(contracts) => contracts,
522 Err(_) => {
523 self.set_retry_backoff(RETRY_GUARD_DEFAULT_BACKOFF);
524 return Err(HttpClientError::Status(Status::InternalFailure).into());
525 }
526 };
527
528 if self.no_updates(&contracts) {
529 self.finish_polling();
530 Ok(PollContractsResponse::Finish)
531 } else {
532 self.log_invalid_contracts(&contracts);
533 self.update_data(&contracts);
534 self.update_links(&contracts);
535 Ok(PollContractsResponse::Continue)
536 }
537 }
538 401 => Ok(PollContractsResponse::Renegotiate),
539 429 => {
540 let backoff = Self::retry_after_backoff(response.header("Retry-After"));
542 self.set_retry_backoff(backoff);
543 logger::warn!(
544 "Contracts service rate limited API {} (429). Backing off {}s.",
545 self.api_id,
546 backoff.as_secs()
547 );
548 Err(InternalUpdateError::UnexpectedResponse(429))
549 }
550 n => {
551 self.set_retry_backoff(RETRY_GUARD_DEFAULT_BACKOFF);
552 debug!(
553 "Fetching contracts failed with status code: {} and body:\n {}",
554 n,
555 String::from_utf8_lossy(response.body())
556 );
557 Err(InternalUpdateError::UnexpectedResponse(n))
558 }
559 }
560 }
561
562 fn set_retry_backoff(&self, backoff: Duration) {
567 let until = self.clock.get_current_time() + backoff;
568 self.contract_storage.set_retry_after(until);
569 }
570
571 fn retry_after_backoff(header: Option<&String>) -> Duration {
575 match header.and_then(|value| value.trim().parse::<u64>().ok()) {
576 Some(seconds) => Duration::from_secs(seconds)
577 .clamp(RETRY_GUARD_DEFAULT_BACKOFF, RETRY_GUARD_MAX_BACKOFF),
578 None => RETRY_GUARD_DEFAULT_BACKOFF,
579 }
580 }
581
582 fn log_invalid_contracts(&self, response: &ContractsResponse) {
583 for invalid_contract_error_msg in response.verify_contracts().err().unwrap_or_default() {
584 logger::warn!("{invalid_contract_error_msg}")
585 }
586 }
587
588 fn no_updates(&self, response: &ContractsResponse) -> bool {
589 let links = response.get_links();
590 links.self_link() == links.next_link()
591 }
592
593 fn finish_polling(&self) {
594 self.contract_storage.update_last();
595 debug!(
596 "No more contracts updates for API {}, polling in next tick.",
597 self.api_id
598 );
599 }
600
601 fn update_data(&self, response: &ContractsResponse) {
602 let data = response.get_data();
603 for contract_event in data {
604 match contract_event.removed.unwrap_or(false) {
605 true => self
606 .contract_storage
607 .remove_contract(&contract_event.client_id),
608 false => self
609 .contract_storage
610 .save_contract(contract_from_event(contract_event)),
611 }
612 }
613 self.contract_storage.update_last();
614 debug!(
615 "{} contract events processed for API {}",
616 data.len(),
617 self.api_id
618 );
619 }
620
621 fn update_links(&self, response: &ContractsResponse) {
622 let links = response.get_links();
623 let params = ContractsRequestParams::new(
624 Some(links.next_link().to_string()),
625 ACCEPT_HASH_ALGORITHM_VALUE.to_string(),
626 );
627 self.contract_storage.save_contracts_request_params(params);
628 }
629
630 fn next_url(&self) -> Option<String> {
631 self.contract_storage
632 .get_contracts_request_params()
633 .and_then(|x| x.next_url)
634 }
635}
636
637#[derive(thiserror::Error, Debug)]
638pub enum ExtractionError {
639 #[error("Api metadata is unavailable.")]
640 ApiMetadata,
641
642 #[error("Environment Context is unavailable.")]
643 EnvironmentContext,
644
645 #[error("Anypoint Context is unavailable.")]
646 AnypointContext,
647}
648
649impl FromContext<ConfigureContext> for ContractValidator {
650 type Error = ExtractionError;
651
652 fn from_context(context: &ConfigureContext) -> Result<Self, Self::Error> {
653 let metadata: Metadata = context.extract_always();
654 let api_id = metadata
655 .api_metadata
656 .id
657 .ok_or(ExtractionError::ApiMetadata)?;
658 let client = context.extract()?;
659 let clock = context.extract_always();
660 let shared_data = context.extract_always();
661 let lock_builder = context.extract_always();
662 let storage_builder: DataStorageBuilder = context
663 .extract()
664 .map_err(|_| ExtractionError::EnvironmentContext)?;
665
666 Ok(Self::new(
667 client,
668 api_id,
669 clock,
670 shared_data,
671 lock_builder,
672 storage_builder,
673 ))
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680
681 #[test]
682 fn retry_after_absent_uses_default() {
683 assert_eq!(
684 ContractValidator::retry_after_backoff(None),
685 RETRY_GUARD_DEFAULT_BACKOFF
686 );
687 }
688
689 #[test]
690 fn retry_after_unparseable_uses_default() {
691 let header = "Fri, 31 Dec 1999 23:59:59 GMT".to_string();
692 assert_eq!(
693 ContractValidator::retry_after_backoff(Some(&header)),
694 RETRY_GUARD_DEFAULT_BACKOFF
695 );
696 }
697
698 #[test]
699 fn retry_after_delta_seconds_is_honored() {
700 let header = "42".to_string();
701 assert_eq!(
702 ContractValidator::retry_after_backoff(Some(&header)),
703 Duration::from_secs(42)
704 );
705 }
706
707 #[test]
708 fn retry_after_trims_whitespace() {
709 let header = " 30 ".to_string();
710 assert_eq!(
711 ContractValidator::retry_after_backoff(Some(&header)),
712 Duration::from_secs(30)
713 );
714 }
715
716 #[test]
717 fn retry_after_is_clamped_to_max() {
718 let header = "100000".to_string();
719 assert_eq!(
720 ContractValidator::retry_after_backoff(Some(&header)),
721 RETRY_GUARD_MAX_BACKOFF
722 );
723 }
724
725 #[test]
726 fn retry_after_below_default_is_floored() {
727 let header = "1".to_string();
729 assert_eq!(
730 ContractValidator::retry_after_backoff(Some(&header)),
731 RETRY_GUARD_DEFAULT_BACKOFF
732 );
733 }
734}