Refine Rust architecture boundaries
Deploy / deploy (push) Successful in 1m33s
CI / Rust Checks (push) Failing after 5m47s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Deployment Manifests (push) Has been skipped

This commit is contained in:
github-ops
2026-06-21 08:58:32 +00:00
parent a31862aeeb
commit 7df9b48513
18 changed files with 1376 additions and 1052 deletions
+31
View File
@@ -6,6 +6,37 @@ mod postgres;
pub use error::RegistryError;
pub use ext::{ExtensionMigration, RegistryExtension, apply_extension_migrations};
pub mod records {
pub use crate::model::{
AgentSummary, AgentVersionRecord, AuthUserRecord, DescriptorKind, DescriptorMetadata,
InvitationRecord, InvocationLogRecord, MembershipRecord, OperationAgentRef,
OperationSampleMetadata, OperationSummary, OperationUsageSummary, OperationVersionRecord,
Page, PlatformApiKeyRecord, PublishedAgentTool, RegistryOperation, SampleKind,
SecretRecord, SecretVersionRecord, SessionRecord, UsageAgentBreakdown, UsageBucket,
UsageOperationBreakdown, UsageRollupRecord, UsageSummary, UsageTimelinePoint,
WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream, WorkspaceUpstreamId,
YamlImportJob, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
};
}
pub mod requests {
pub use crate::model::{
CreateAgentDraftVersionRequest, CreateAgentRequest, CreateInvitationRequest,
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
ListInvocationLogsQuery, PublishAgentRequest, PublishRequest, RotateSecretRequest,
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, UpdateWorkspaceRequest,
UsageQuery,
};
}
pub mod infrastructure {
pub use crate::ext::{ExtensionMigration, RegistryExtension, apply_extension_migrations};
pub use crate::postgres::{PostgresPoolConfig, PostgresPoolConfigError, PostgresRegistry};
}
pub use model::{
AgentSummary, AgentVersionRecord, AuthUserRecord, CreateAgentDraftVersionRequest,
CreateAgentRequest, CreateInvitationRequest, CreateInvocationLogRequest,
@@ -0,0 +1,70 @@
use std::time::Duration;
use sqlx::{
PgPool,
postgres::{PgConnectOptions, PgPoolOptions},
};
use crate::{error::RegistryError, migrations};
use super::{PostgresPoolConfig, PostgresRegistry};
impl PostgresRegistry {
pub async fn connect(database_url: &str) -> Result<Self, RegistryError> {
Self::connect_in_schema(database_url, None).await
}
pub async fn connect_with_options(
connect_options: PgConnectOptions,
) -> Result<Self, RegistryError> {
Self::connect_with_options_and_pool_config(connect_options, PostgresPoolConfig::default())
.await
}
pub async fn connect_with_options_and_pool_config(
connect_options: PgConnectOptions,
pool_config: PostgresPoolConfig,
) -> Result<Self, RegistryError> {
let pool = PgPoolOptions::new()
.max_connections(pool_config.max_connections)
.min_connections(pool_config.min_connections)
.acquire_timeout(Duration::from_millis(pool_config.acquire_timeout_ms))
.idle_timeout(Duration::from_millis(pool_config.idle_timeout_ms))
.max_lifetime(Duration::from_millis(pool_config.max_lifetime_ms))
.connect_with(connect_options)
.await?;
migrations::apply_postgres(&pool).await?;
Ok(Self { pool })
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
pub async fn migrate(&self) -> Result<(), RegistryError> {
migrations::apply_postgres(&self.pool).await?;
Ok(())
}
async fn connect_in_schema(
database_url: &str,
schema: Option<&str>,
) -> Result<Self, RegistryError> {
let pool = PgPoolOptions::new()
.min_connections(0)
.max_connections(1)
.idle_timeout(std::time::Duration::from_secs(1))
.connect(database_url)
.await?;
if let Some(schema) = schema {
sqlx::query(&format!("set search_path to {schema}"))
.execute(&pool)
.await?;
}
let registry = Self { pool };
registry.migrate().await?;
Ok(registry)
}
}
+5 -269
View File
@@ -1,8 +1,10 @@
mod agent;
mod api_key;
mod auth;
mod connection;
mod observability;
mod operation;
mod pool_config;
mod secret;
mod upstream;
mod workspace;
@@ -16,18 +18,13 @@ use crank_core::{
};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use sqlx::{
PgPool, Postgres, Row, Transaction,
postgres::{PgConnectOptions, PgPoolOptions, PgRow},
types::Json,
};
use std::{collections::BTreeMap, env, time::Duration};
use thiserror::Error;
use sqlx::{PgPool, Postgres, Row, Transaction, postgres::PgRow, types::Json};
use time::OffsetDateTime;
pub use pool_config::{PostgresPoolConfig, PostgresPoolConfigError};
use crate::{
error::RegistryError,
migrations,
model::{
AgentSummary, AgentVersionRecord, AuthUserRecord, CreateAgentDraftVersionRequest,
CreateAgentRequest, CreateInvitationRequest, CreateInvocationLogRequest,
@@ -50,156 +47,7 @@ pub struct PostgresRegistry {
pool: PgPool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PostgresPoolConfig {
pub max_connections: u32,
pub min_connections: u32,
pub acquire_timeout_ms: u64,
pub idle_timeout_ms: u64,
pub max_lifetime_ms: u64,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PostgresPoolConfigError {
#[error("invalid postgres pool setting {name}={value}")]
InvalidValue { name: &'static str, value: String },
#[error("POSTGRES_MAX_CONNECTIONS must be greater than zero")]
ZeroMaxConnections,
#[error("POSTGRES_MIN_CONNECTIONS must not exceed POSTGRES_MAX_CONNECTIONS")]
MinConnectionsExceedMax,
}
impl Default for PostgresPoolConfig {
fn default() -> Self {
Self {
max_connections: 20,
min_connections: 2,
acquire_timeout_ms: 5_000,
idle_timeout_ms: 600_000,
max_lifetime_ms: 1_800_000,
}
}
}
impl PostgresPoolConfig {
pub fn from_env() -> Result<Self, PostgresPoolConfigError> {
Self::from_vars(env::vars())
}
fn from_vars<I, K, V>(vars: I) -> Result<Self, PostgresPoolConfigError>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let vars = vars
.into_iter()
.map(|(name, value)| (name.as_ref().to_owned(), value.as_ref().to_owned()))
.collect::<BTreeMap<_, _>>();
let defaults = Self::default();
let config = Self {
max_connections: parse_u32_setting(
&vars,
"POSTGRES_MAX_CONNECTIONS",
defaults.max_connections,
)?,
min_connections: parse_u32_setting(
&vars,
"POSTGRES_MIN_CONNECTIONS",
defaults.min_connections,
)?,
acquire_timeout_ms: parse_u64_setting(
&vars,
"POSTGRES_ACQUIRE_TIMEOUT_MS",
defaults.acquire_timeout_ms,
)?,
idle_timeout_ms: parse_u64_setting(
&vars,
"POSTGRES_IDLE_TIMEOUT_MS",
defaults.idle_timeout_ms,
)?,
max_lifetime_ms: parse_u64_setting(
&vars,
"POSTGRES_MAX_LIFETIME_MS",
defaults.max_lifetime_ms,
)?,
};
config.validate()?;
Ok(config)
}
fn validate(self) -> Result<Self, PostgresPoolConfigError> {
if self.max_connections == 0 {
return Err(PostgresPoolConfigError::ZeroMaxConnections);
}
if self.min_connections > self.max_connections {
return Err(PostgresPoolConfigError::MinConnectionsExceedMax);
}
Ok(self)
}
}
impl PostgresRegistry {
pub async fn connect(database_url: &str) -> Result<Self, RegistryError> {
Self::connect_in_schema(database_url, None).await
}
pub async fn connect_with_options(
connect_options: PgConnectOptions,
) -> Result<Self, RegistryError> {
Self::connect_with_options_and_pool_config(connect_options, PostgresPoolConfig::default())
.await
}
pub async fn connect_with_options_and_pool_config(
connect_options: PgConnectOptions,
pool_config: PostgresPoolConfig,
) -> Result<Self, RegistryError> {
let pool = PgPoolOptions::new()
.max_connections(pool_config.max_connections)
.min_connections(pool_config.min_connections)
.acquire_timeout(Duration::from_millis(pool_config.acquire_timeout_ms))
.idle_timeout(Duration::from_millis(pool_config.idle_timeout_ms))
.max_lifetime(Duration::from_millis(pool_config.max_lifetime_ms))
.connect_with(connect_options)
.await?;
migrations::apply_postgres(&pool).await?;
Ok(Self { pool })
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
pub async fn migrate(&self) -> Result<(), RegistryError> {
migrations::apply_postgres(&self.pool).await?;
Ok(())
}
async fn connect_in_schema(
database_url: &str,
schema: Option<&str>,
) -> Result<Self, RegistryError> {
let pool = PgPoolOptions::new()
.min_connections(0)
.max_connections(1)
.idle_timeout(std::time::Duration::from_secs(1))
.connect(database_url)
.await?;
if let Some(schema) = schema {
sqlx::query(&format!("set search_path to {schema}"))
.execute(&pool)
.await?;
}
let registry = Self { pool };
registry.migrate().await?;
Ok(registry)
}
async fn list_agent_bindings(
&self,
agent_id: &AgentId,
@@ -226,42 +74,6 @@ impl PostgresRegistry {
}
}
fn parse_u32_setting(
vars: &BTreeMap<String, String>,
name: &'static str,
default: u32,
) -> Result<u32, PostgresPoolConfigError> {
vars.get(name)
.map(|value| {
value
.parse::<u32>()
.map_err(|_| PostgresPoolConfigError::InvalidValue {
name,
value: value.clone(),
})
})
.transpose()
.map(|value| value.unwrap_or(default))
}
fn parse_u64_setting(
vars: &BTreeMap<String, String>,
name: &'static str,
default: u64,
) -> Result<u64, PostgresPoolConfigError> {
vars.get(name)
.map(|value| {
value
.parse::<u64>()
.map_err(|_| PostgresPoolConfigError::InvalidValue {
name,
value: value.clone(),
})
})
.transpose()
.map(|value| value.unwrap_or(default))
}
async fn insert_version_row(
tx: &mut Transaction<'_, Postgres>,
snapshot: &RegistryOperation,
@@ -930,79 +742,3 @@ fn usage_bucket_sql(bucket: crate::model::UsageBucket) -> &'static str {
crate::model::UsageBucket::Month => "month",
}
}
#[cfg(test)]
mod pool_config_tests {
use super::{PostgresPoolConfig, PostgresPoolConfigError};
#[test]
fn pool_config_uses_explicit_defaults() {
let config = PostgresPoolConfig::from_vars(std::iter::empty::<(&str, &str)>()).unwrap();
assert_eq!(
config,
PostgresPoolConfig {
max_connections: 20,
min_connections: 2,
acquire_timeout_ms: 5_000,
idle_timeout_ms: 600_000,
max_lifetime_ms: 1_800_000,
}
);
}
#[test]
fn pool_config_parses_overrides() {
let config = PostgresPoolConfig::from_vars([
("POSTGRES_MAX_CONNECTIONS", "32"),
("POSTGRES_MIN_CONNECTIONS", "4"),
("POSTGRES_ACQUIRE_TIMEOUT_MS", "7000"),
("POSTGRES_IDLE_TIMEOUT_MS", "900000"),
("POSTGRES_MAX_LIFETIME_MS", "3600000"),
])
.unwrap();
assert_eq!(
config,
PostgresPoolConfig {
max_connections: 32,
min_connections: 4,
acquire_timeout_ms: 7_000,
idle_timeout_ms: 900_000,
max_lifetime_ms: 3_600_000,
}
);
}
#[test]
fn pool_config_rejects_invalid_numeric_value() {
let error =
PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "abc")]).unwrap_err();
assert_eq!(
error,
PostgresPoolConfigError::InvalidValue {
name: "POSTGRES_MAX_CONNECTIONS",
value: "abc".to_owned(),
}
);
}
#[test]
fn pool_config_rejects_zero_max_connections() {
let error = PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "0")]).unwrap_err();
assert_eq!(error, PostgresPoolConfigError::ZeroMaxConnections);
}
#[test]
fn pool_config_rejects_min_connections_above_max() {
let error = PostgresPoolConfig::from_vars([
("POSTGRES_MAX_CONNECTIONS", "2"),
("POSTGRES_MIN_CONNECTIONS", "3"),
])
.unwrap_err();
assert_eq!(error, PostgresPoolConfigError::MinConnectionsExceedMax);
}
}
@@ -0,0 +1,206 @@
use std::{collections::BTreeMap, env};
use thiserror::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PostgresPoolConfig {
pub max_connections: u32,
pub min_connections: u32,
pub acquire_timeout_ms: u64,
pub idle_timeout_ms: u64,
pub max_lifetime_ms: u64,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PostgresPoolConfigError {
#[error("invalid postgres pool setting {name}={value}")]
InvalidValue { name: &'static str, value: String },
#[error("POSTGRES_MAX_CONNECTIONS must be greater than zero")]
ZeroMaxConnections,
#[error("POSTGRES_MIN_CONNECTIONS must not exceed POSTGRES_MAX_CONNECTIONS")]
MinConnectionsExceedMax,
}
impl Default for PostgresPoolConfig {
fn default() -> Self {
Self {
max_connections: 20,
min_connections: 2,
acquire_timeout_ms: 5_000,
idle_timeout_ms: 600_000,
max_lifetime_ms: 1_800_000,
}
}
}
impl PostgresPoolConfig {
pub fn from_env() -> Result<Self, PostgresPoolConfigError> {
Self::from_vars(env::vars())
}
fn from_vars<I, K, V>(vars: I) -> Result<Self, PostgresPoolConfigError>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let vars = vars
.into_iter()
.map(|(name, value)| (name.as_ref().to_owned(), value.as_ref().to_owned()))
.collect::<BTreeMap<_, _>>();
let defaults = Self::default();
let config = Self {
max_connections: parse_u32_setting(
&vars,
"POSTGRES_MAX_CONNECTIONS",
defaults.max_connections,
)?,
min_connections: parse_u32_setting(
&vars,
"POSTGRES_MIN_CONNECTIONS",
defaults.min_connections,
)?,
acquire_timeout_ms: parse_u64_setting(
&vars,
"POSTGRES_ACQUIRE_TIMEOUT_MS",
defaults.acquire_timeout_ms,
)?,
idle_timeout_ms: parse_u64_setting(
&vars,
"POSTGRES_IDLE_TIMEOUT_MS",
defaults.idle_timeout_ms,
)?,
max_lifetime_ms: parse_u64_setting(
&vars,
"POSTGRES_MAX_LIFETIME_MS",
defaults.max_lifetime_ms,
)?,
};
config.validate()?;
Ok(config)
}
fn validate(self) -> Result<Self, PostgresPoolConfigError> {
if self.max_connections == 0 {
return Err(PostgresPoolConfigError::ZeroMaxConnections);
}
if self.min_connections > self.max_connections {
return Err(PostgresPoolConfigError::MinConnectionsExceedMax);
}
Ok(self)
}
}
fn parse_u32_setting(
vars: &BTreeMap<String, String>,
name: &'static str,
default: u32,
) -> Result<u32, PostgresPoolConfigError> {
vars.get(name)
.map(|value| {
value
.parse::<u32>()
.map_err(|_| PostgresPoolConfigError::InvalidValue {
name,
value: value.clone(),
})
})
.transpose()
.map(|value| value.unwrap_or(default))
}
fn parse_u64_setting(
vars: &BTreeMap<String, String>,
name: &'static str,
default: u64,
) -> Result<u64, PostgresPoolConfigError> {
vars.get(name)
.map(|value| {
value
.parse::<u64>()
.map_err(|_| PostgresPoolConfigError::InvalidValue {
name,
value: value.clone(),
})
})
.transpose()
.map(|value| value.unwrap_or(default))
}
#[cfg(test)]
mod pool_config_tests {
use super::{PostgresPoolConfig, PostgresPoolConfigError};
#[test]
fn pool_config_uses_explicit_defaults() {
let config = PostgresPoolConfig::from_vars(std::iter::empty::<(&str, &str)>()).unwrap();
assert_eq!(
config,
PostgresPoolConfig {
max_connections: 20,
min_connections: 2,
acquire_timeout_ms: 5_000,
idle_timeout_ms: 600_000,
max_lifetime_ms: 1_800_000,
}
);
}
#[test]
fn pool_config_parses_overrides() {
let config = PostgresPoolConfig::from_vars([
("POSTGRES_MAX_CONNECTIONS", "32"),
("POSTGRES_MIN_CONNECTIONS", "4"),
("POSTGRES_ACQUIRE_TIMEOUT_MS", "7000"),
("POSTGRES_IDLE_TIMEOUT_MS", "900000"),
("POSTGRES_MAX_LIFETIME_MS", "3600000"),
])
.unwrap();
assert_eq!(
config,
PostgresPoolConfig {
max_connections: 32,
min_connections: 4,
acquire_timeout_ms: 7_000,
idle_timeout_ms: 900_000,
max_lifetime_ms: 3_600_000,
}
);
}
#[test]
fn pool_config_rejects_invalid_numeric_value() {
let error =
PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "abc")]).unwrap_err();
assert_eq!(
error,
PostgresPoolConfigError::InvalidValue {
name: "POSTGRES_MAX_CONNECTIONS",
value: "abc".to_owned(),
}
);
}
#[test]
fn pool_config_rejects_zero_max_connections() {
let error = PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "0")]).unwrap_err();
assert_eq!(error, PostgresPoolConfigError::ZeroMaxConnections);
}
#[test]
fn pool_config_rejects_min_connections_above_max() {
let error = PostgresPoolConfig::from_vars([
("POSTGRES_MAX_CONNECTIONS", "2"),
("POSTGRES_MIN_CONNECTIONS", "3"),
])
.unwrap_err();
assert_eq!(error, PostgresPoolConfigError::MinConnectionsExceedMax);
}
}