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