systemprompt_database/repository/service/
repo.rs1use std::sync::Arc;
13
14use sqlx::PgPool;
15use systemprompt_identifiers::{InstanceId, ServiceName};
16use systemprompt_manifest::services::ServiceStatus;
17
18use super::model::{CreateServiceInput, ServiceConfig, ServiceRow, UpsertServiceProcessInput};
19use crate::DbPool;
20use crate::error::DatabaseResult;
21
22#[derive(Debug, Clone)]
23pub struct ServiceRepository {
24 pub(super) pool: Arc<PgPool>,
25 pub(super) write_pool: Arc<PgPool>,
26 pub(super) session_pool: Arc<PgPool>,
27 pub(super) instance_id: InstanceId,
28}
29
30impl ServiceRepository {
31 pub fn new(db: &DbPool, instance_id: InstanceId) -> Self {
32 let pool = db.pool();
33 let write_pool = db.write_pool();
34 let session_pool = db.session_pool();
35 Self {
36 pool,
37 write_pool,
38 session_pool,
39 instance_id,
40 }
41 }
42
43 pub const fn instance_id(&self) -> &InstanceId {
44 &self.instance_id
45 }
46
47 pub async fn find_service_by_name(
48 &self,
49 name: &ServiceName,
50 ) -> DatabaseResult<Option<ServiceConfig>> {
51 let row = sqlx::query_as!(
52 ServiceRow,
53 r#"
54 SELECT instance_id as "instance_id: InstanceId", name, module_name, status, pid, port, binary_mtime,
55 heartbeat_at::text as "heartbeat_at!",
56 created_at::text as "created_at!", updated_at::text as "updated_at!"
57 FROM services
58 WHERE instance_id = $1 AND name = $2
59 "#,
60 self.instance_id.as_str(),
61 name.as_str()
62 )
63 .fetch_optional(&*self.pool)
64 .await?;
65 row.map(ServiceRow::into_config).transpose()
66 }
67
68 pub async fn create_service(&self, input: CreateServiceInput<'_>) -> DatabaseResult<()> {
69 let port_i32 = i32::from(input.port);
70 sqlx::query!(
71 r#"
72 INSERT INTO services (instance_id, name, module_name, status, port, binary_mtime)
73 VALUES ($1, $2, $3, $4, $5, $6)
74 ON CONFLICT (instance_id, name) DO UPDATE SET
75 module_name = EXCLUDED.module_name,
76 status = EXCLUDED.status,
77 port = EXCLUDED.port,
78 binary_mtime = EXCLUDED.binary_mtime,
79 heartbeat_at = CURRENT_TIMESTAMP,
80 updated_at = CURRENT_TIMESTAMP
81 "#,
82 self.instance_id.as_str(),
83 input.name.as_str(),
84 input.module_name.as_str(),
85 input.status.as_str(),
86 port_i32,
87 input.binary_mtime
88 )
89 .execute(&*self.write_pool)
90 .await?;
91 Ok(())
92 }
93
94 pub async fn upsert_service_process(
95 &self,
96 input: UpsertServiceProcessInput<'_>,
97 ) -> DatabaseResult<()> {
98 let port_i32 = i32::from(input.port);
99 sqlx::query!(
100 r#"
101 INSERT INTO services (instance_id, name, module_name, pid, port, status, updated_at)
102 VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP)
103 ON CONFLICT (instance_id, name) DO UPDATE SET
104 pid = EXCLUDED.pid,
105 port = EXCLUDED.port,
106 status = EXCLUDED.status,
107 heartbeat_at = CURRENT_TIMESTAMP,
108 updated_at = CURRENT_TIMESTAMP
109 "#,
110 self.instance_id.as_str(),
111 input.name.as_str(),
112 input.module_name.as_str(),
113 input.pid,
114 port_i32,
115 input.status.as_str()
116 )
117 .execute(&*self.write_pool)
118 .await?;
119 Ok(())
120 }
121
122 pub async fn update_service_status(
123 &self,
124 service_name: &ServiceName,
125 status: ServiceStatus,
126 ) -> DatabaseResult<()> {
127 sqlx::query!(
128 r#"UPDATE services SET status = $1, updated_at = CURRENT_TIMESTAMP
129 WHERE instance_id = $2 AND name = $3"#,
130 status.as_str(),
131 self.instance_id.as_str(),
132 service_name.as_str()
133 )
134 .execute(&*self.write_pool)
135 .await?;
136 Ok(())
137 }
138
139 pub async fn delete_service(&self, service_name: &ServiceName) -> DatabaseResult<()> {
140 sqlx::query!(
141 r#"DELETE FROM services WHERE instance_id = $1 AND name = $2"#,
142 self.instance_id.as_str(),
143 service_name.as_str()
144 )
145 .execute(&*self.write_pool)
146 .await?;
147 Ok(())
148 }
149
150 pub async fn update_service_pid(
151 &self,
152 service_name: &ServiceName,
153 pid: i32,
154 ) -> DatabaseResult<()> {
155 sqlx::query!(
156 r#"UPDATE services SET pid = $1, updated_at = CURRENT_TIMESTAMP
157 WHERE instance_id = $2 AND name = $3"#,
158 pid,
159 self.instance_id.as_str(),
160 service_name.as_str()
161 )
162 .execute(&*self.write_pool)
163 .await?;
164 Ok(())
165 }
166
167 pub async fn update_service_port(
168 &self,
169 service_name: &ServiceName,
170 port: u16,
171 ) -> DatabaseResult<()> {
172 let port_i32 = i32::from(port);
173 sqlx::query!(
174 r#"UPDATE services SET port = $1, updated_at = CURRENT_TIMESTAMP
175 WHERE instance_id = $2 AND name = $3"#,
176 port_i32,
177 self.instance_id.as_str(),
178 service_name.as_str()
179 )
180 .execute(&*self.write_pool)
181 .await?;
182 Ok(())
183 }
184
185 pub async fn clear_service_pid(&self, service_name: &ServiceName) -> DatabaseResult<()> {
186 sqlx::query!(
187 r#"UPDATE services SET pid = NULL, updated_at = CURRENT_TIMESTAMP
188 WHERE instance_id = $1 AND name = $2"#,
189 self.instance_id.as_str(),
190 service_name.as_str()
191 )
192 .execute(&*self.write_pool)
193 .await?;
194 Ok(())
195 }
196
197 pub async fn mark_service_crashed(&self, service_name: &ServiceName) -> DatabaseResult<()> {
198 sqlx::query!(
199 r#"UPDATE services SET status = 'error', pid = NULL, updated_at = CURRENT_TIMESTAMP
200 WHERE instance_id = $1 AND name = $2"#,
201 self.instance_id.as_str(),
202 service_name.as_str()
203 )
204 .execute(&*self.write_pool)
205 .await?;
206 Ok(())
207 }
208
209 pub async fn update_service_stopped(&self, service_name: &ServiceName) -> DatabaseResult<()> {
210 sqlx::query!(
211 r#"UPDATE services SET status = 'stopped', pid = NULL, updated_at = CURRENT_TIMESTAMP
212 WHERE instance_id = $1 AND name = $2"#,
213 self.instance_id.as_str(),
214 service_name.as_str()
215 )
216 .execute(&*self.write_pool)
217 .await?;
218 Ok(())
219 }
220}