systemprompt_database/repository/service/
claim.rs1use sqlx::{Connection, PgConnection};
27use systemprompt_identifiers::InstanceId;
28use systemprompt_traits::RepositoryError;
29use tracing::warn;
30
31use super::repo::ServiceRepository;
32
33pub const INSTANCE_CLAIM_CLASS: i32 = 0x5350_0001;
37
38pub const CLAIM_DEAD_PEER_SECS: u64 = 25;
39
40#[derive(Debug, thiserror::Error)]
42pub enum InstanceClaimError {
43 #[error(
44 "instance id '{instance_id}' is already claimed by a live process; give each replica its \
45 own server.instance_id or leave it unset so HOSTNAME is used"
46 )]
47 Claimed { instance_id: InstanceId },
48
49 #[error("could not claim instance id: {0}")]
50 Repository(#[from] RepositoryError),
51}
52
53impl From<sqlx::Error> for InstanceClaimError {
54 fn from(err: sqlx::Error) -> Self {
55 Self::Repository(RepositoryError::from(err))
56 }
57}
58
59pub struct InstanceClaim {
61 session: Option<PgConnection>,
62 instance_id: InstanceId,
63}
64
65impl InstanceClaim {
66 pub const fn instance_id(&self) -> &InstanceId {
67 &self.instance_id
68 }
69
70 pub async fn release(mut self) {
71 if let Some(mut session) = self.session.take() {
72 if let Err(e) = sqlx::query_scalar!(
73 "SELECT pg_advisory_unlock($1, hashtext($2))",
74 INSTANCE_CLAIM_CLASS,
75 self.instance_id.as_str()
76 )
77 .fetch_one(&mut session)
78 .await
79 {
80 warn!(
81 instance_id = %self.instance_id,
82 error = %e,
83 "Failed to release instance claim; closing its session"
84 );
85 }
86 if let Err(e) = session.close().await {
87 warn!(
88 instance_id = %self.instance_id,
89 error = %e,
90 "Failed to close instance claim session"
91 );
92 }
93 }
94 }
95}
96
97impl std::fmt::Debug for InstanceClaim {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.debug_struct("InstanceClaim")
100 .field("instance_id", &self.instance_id)
101 .field("held", &self.session.is_some())
102 .finish()
103 }
104}
105
106impl ServiceRepository {
107 pub async fn claim_instance(&self) -> Result<InstanceClaim, InstanceClaimError> {
108 let mut session = self.session_pool.acquire().await?.detach();
109 sqlx::query_scalar!(
113 r#"SELECT set_config('tcp_keepalives_idle', '10', false)
114 || set_config('tcp_keepalives_interval', '5', false)
115 || set_config('tcp_keepalives_count', '3', false) AS "applied!""#
116 )
117 .fetch_one(&mut session)
118 .await?;
119 let acquired = sqlx::query_scalar!(
120 r#"SELECT pg_try_advisory_lock($1, hashtext($2)) AS "acquired!""#,
121 INSTANCE_CLAIM_CLASS,
122 self.instance_id.as_str()
123 )
124 .fetch_one(&mut session)
125 .await?;
126
127 if !acquired {
128 if let Err(e) = session.close().await {
129 warn!(
130 instance_id = %self.instance_id,
131 error = %e,
132 "Failed to close refused claim session"
133 );
134 }
135 return Err(InstanceClaimError::Claimed {
136 instance_id: self.instance_id.clone(),
137 });
138 }
139
140 Ok(InstanceClaim {
141 session: Some(session),
142 instance_id: self.instance_id.clone(),
143 })
144 }
145}