1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
use serde::{Deserialize, Serialize};
use crate::config::validation::{require_nonempty, require_nonzero};
use crate::errors::OrionError;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct StorageConfig {
/// Database connection URL.
/// Examples: "sqlite:orion.db", "postgres://user:pass@host/db", "mysql://user:pass@host/db"
pub url: String,
pub max_connections: u32,
/// Minimum number of connections to maintain in the pool (0 = no minimum).
pub min_connections: u32,
/// SQLite busy timeout in milliseconds (ignored for other backends).
pub busy_timeout_ms: u64,
/// Connection pool acquire timeout in seconds.
pub acquire_timeout_secs: u64,
/// Maximum idle time in seconds before a connection is closed (0 = no limit).
pub idle_timeout_secs: u64,
/// Encrypt `connectors.config_json` at rest (H3). Empty (the default)
/// stores connector configs as plaintext JSON. Set to the 64-hex-char
/// encoding of a 32-byte key (`openssl rand -hex 32`) and every connector
/// write is AES-256-GCM-encrypted before it reaches the database; reads
/// decrypt transparently, and pre-existing plaintext rows keep loading
/// until their next write re-encrypts them. Prefer the environment form
/// (`ORION_STORAGE__CONNECTOR_ENCRYPTION_KEY`) over the config file — a
/// key in the file sits beside the database it protects.
pub connector_encryption_key: String,
/// Directory for database backup files (SQLite only).
pub backup_dir: String,
/// Keep only the newest N `orion_backup_*.db` files in `backup_dir`,
/// pruning older ones after each successful backup. Unset (the default)
/// keeps every backup — but they accumulate on the same disk as the live
/// database, so bounded deployments should set this. Must be >= 1 when
/// set (SQLite only).
pub backup_retention_count: Option<u32>,
/// Run pending migrations automatically at startup. Set `false` in
/// multi-replica deployments so boot-racing replicas never migrate;
/// run `orion-server migrate` as a deploy step instead. Startup fails
/// hard when this is `false` and migrations are pending.
pub auto_migrate: bool,
/// How long to keep retrying the initial database connection at startup,
/// in seconds (D14). `0` fails fast on the first error.
///
/// A Postgres/MySQL failover takes tens of seconds; without a retry window
/// every replica exits, and the container restart backoff then outlives
/// the outage. Retries are bounded by this window with a fixed exponential
/// backoff, and the *migration* check stays fail-fast. Ignored for SQLite,
/// whose connect failures (bad path, permissions, corrupt file) are not
/// transient.
pub connect_retry_secs: u64,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
url: "sqlite:orion.db".to_string(),
max_connections: 50,
min_connections: 5,
busy_timeout_ms: 5000,
acquire_timeout_secs: 3,
idle_timeout_secs: 300,
connector_encryption_key: String::new(),
backup_dir: "./backups".to_string(),
backup_retention_count: None,
auto_migrate: true,
connect_retry_secs: 60,
}
}
}
impl StorageConfig {
pub(crate) fn validate(&self) -> Result<(), OrionError> {
require_nonempty(&self.url, "storage.url")?;
require_nonzero(self.busy_timeout_ms, "storage.busy_timeout_ms")?;
require_nonzero(self.acquire_timeout_secs, "storage.acquire_timeout_secs")?;
// 0 would delete the backup just written; "keep none" is not a
// retention policy — leave the field unset to disable pruning.
// H3: fail at startup, not at the first connector read/write.
if !self.connector_encryption_key.is_empty() {
crate::storage::config_encryption::ConfigCipher::from_hex(
&self.connector_encryption_key,
)?;
}
if self.backup_retention_count == Some(0) {
return Err(OrionError::Config {
message: "storage.backup_retention_count must be >= 1 when set \
(unset keeps every backup)"
.to_string(),
});
}
Ok(())
}
}