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