feat: implement the read-only database connection#2356
Conversation
Reviewer's GuideIntroduce explicit read-write and read-only database connection wrappers, add separate read-only DB configuration, and refactor modules/endpoints to consume the appropriate connection type (including enforcing read-only transactions) while keeping existing behavior via sensible fallbacks and updated tests/ADR documentation. Sequence diagram for a read-only SBOM fetch using ReadOnly connectionsequenceDiagram
actor Client
participant Actix as ActixHandler
participant SbomSvc as SbomService
participant DBRO as ReadOnly
participant Tx as DatabaseTransaction
participant Replica as PostgreSQLReplica
Client->>Actix: GET /v2/sbom/{id}
Actix->>DBRO: begin()
DBRO-->>Actix: Result(DatabaseTransaction)
Actix->>SbomSvc: fetch_sbom_summary(id, &Tx)
SbomSvc->>Tx: SELECT ... FROM sbom ...
Tx->>Replica: execute read-only query
Replica-->>Tx: rows
Tx-->>SbomSvc: QueryResult
SbomSvc-->>Actix: Option<SbomSummary>
Actix->>Tx: commit()
Tx-->>DBRO: ok
Actix-->>Client: 200 OK with SBOM summary
Sequence diagram for SBOM upload using ReadWrite connectionsequenceDiagram
actor Client
participant Actix as ActixHandler
participant Ingestor as IngestorService
participant GroupSvc as SbomGroupService
participant DBRW as ReadWrite
participant Tx as DatabaseTransaction
participant Primary as PostgreSQLPrimary
Client->>Actix: POST /v2/sbom/upload
Actix->>Actix: decompress bytes
Actix->>DBRW: begin()
DBRW-->>Actix: DatabaseTransaction
Actix->>Ingestor: ingest(bytes, format, labels, cache, &Tx)
Ingestor->>Tx: INSERT sbom, components, relations
Tx->>Primary: execute writes
Primary-->>Tx: ok
Ingestor-->>Actix: IngestResult{ id }
alt group not empty
Actix->>GroupSvc: update_assignments(id, None, group, &Tx)
GroupSvc->>Tx: INSERT/DELETE group links
Tx->>Primary: execute writes
Primary-->>Tx: ok
end
Actix->>Actix: rewrite result.id with urn:uuid: prefix
Actix->>Tx: commit()
Tx-->>DBRW: ok
Actix-->>Client: 201 Created with SBOM metadata
Class diagram for new read-write and read-only database wrappersclassDiagram
class Database {
+name() &str
+ping() Result
+close() Result
}
class ReadWrite {
-Database inner
+new(db Database) ReadWrite
+name() &str
+ping() Result
+close() Result
+into_inner() Database
}
class ReadOnly {
-Database inner
+new(db Database) ReadOnly
+name() &str
+ping() Result
+close() Result
+begin() Result~DatabaseTransaction,DbError~
+begin_with_config(isolation_level Option~IsolationLevel~, access_mode Option~AccessMode~) Result~DatabaseTransaction,DbError~
-validate_access_mode(mode Option~AccessMode~) Result~Option~AccessMode~,DbError~
+into_inner() Database
}
class DatabaseReadOnly {
+url Option~String~
+username Option~String~
+password Option~Hide~String~~
+host Option~String~
+port Option~u16~
+name Option~String~
+max_conn Option~u32~
+min_conn Option~u32~
+sslmode Option~SslMode~
+connect_timeout Option~u64~
+acquire_timeout Option~u64~
+max_lifetime Option~u64~
+idle_timeout Option~u64~
+is_configured() bool
+to_database_config(fallback Database) Database
}
class DbError {
<<enum>>
Database
Unavailable
ReadOnly
}
class ConnectionTrait {
<<interface>>
+get_database_backend() DbBackend
+execute(stmt Statement) Result~ExecResult,DbErr~
+execute_unprepared(sql &str) Result~ExecResult,DbErr~
+query_one(stmt Statement) Result~Option~QueryResult~,DbErr~
+query_all(stmt Statement) Result~Vec~QueryResult~,DbErr~
+support_returning() bool
}
class StreamTrait {
<<interface>>
+stream(stmt Statement) Future~Stream,DbErr~
}
class TransactionTrait {
<<interface>>
+begin() Result~DatabaseTransaction,DbErr~
+begin_with_config(isolation_level Option~IsolationLevel~, access_mode Option~AccessMode~) Result~DatabaseTransaction,DbErr~
+transaction(callback F) Result~T,TransactionError~E~~
+transaction_with_config(callback F, isolation_level Option~IsolationLevel~, access_mode Option~AccessMode~) Result~T,TransactionError~E~~
}
class IntoSchemaManagerConnection {
<<interface>>
+into_schema_manager_connection() SchemaManagerConnection
}
class DatabaseConnection
ReadWrite o-- Database : wraps
ReadOnly o-- Database : wraps
DatabaseReadOnly --> Database : builds
ReadWrite ..|> ConnectionTrait
ReadWrite ..|> StreamTrait
ReadWrite ..|> TransactionTrait
ReadWrite ..|> IntoSchemaManagerConnection
ReadOnly ..|> IntoSchemaManagerConnection
ReadOnly ..> TransactionTrait : uses begin_with_config
DatabaseConnection ..|> ConnectionTrait
DatabaseConnection ..|> StreamTrait
DatabaseConnection ..|> TransactionTrait
DbError --> DbErr : wraps
DbError ..> HttpResponse : ResponseError
ReadWrite --> DatabaseConnection : Deref
ReadOnly --> DatabaseConnection : used inside begin_with_config
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
6d215af to
e8d3263
Compare
There was a problem hiding this comment.
Sorry @ctron, your pull request is larger than the review limit of 150000 diff characters
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2356 +/- ##
==========================================
- Coverage 70.90% 70.71% -0.19%
==========================================
Files 442 440 -2
Lines 25369 25669 +300
Branches 25369 25669 +300
==========================================
+ Hits 17987 18152 +165
- Misses 6400 6510 +110
- Partials 982 1007 +25 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
I think You may want also update the guidance in CONVENTIONS.md: and maybe hint there when ReadOnly vs ReadWrite should be used (although that may be obvious to people or AI enough). |
|
Also seems to me that with this, the |
queria
left a comment
There was a problem hiding this comment.
I am not sure if the mode without R/W DB configuration, as mentioned in ADR, is possible currently?
| false => None, | ||
| }; | ||
|
|
||
| let db = db::Database::new(&run.database).await?; |
There was a problem hiding this comment.
Database instance is not conditional - at least not conditional like db_ro = database_ro.is_configured() { ... } below.
| } else { | ||
| db::ReadOnly::new(db.clone()) | ||
| }; | ||
| let db_rw = db::ReadWrite::new(db.clone()); |
There was a problem hiding this comment.
And it's then always cloned and below passed to InitData.
There was a problem hiding this comment.
It is used later. But, passing it on to the Analysis service, that should be the readonly connection.
jcrossley3
left a comment
There was a problem hiding this comment.
Why is application-level DB routing required? What existing problem is it solving? And is the configuration complexity trade-off worth it? Whoever will be responsible for that configuration should review this PR.
| The `server/src/profile/api.rs` `configure()` function constructs both wrapper types and passes them | ||
| to each module accordingly. | ||
|
|
||
| ## Alternatives considered |
There was a problem hiding this comment.
No, as we would need to build, productize and support it. It would also be required to incorporated into deployments like RDS.
RDS on the other side offers read-only replicas out of the box.
|
other than git conflict, overall lgtm |
I updated the ADR to explain this better.
Yes. As all we're talking about is adding a new set of DB connection parameters only if this should be enabled. |
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
afd8caa to
0e0d3cd
Compare




Implement a second DB connection which is only used for read-only operations. Defaulting that connection to the primary (read-write) one.
The idea is to configure the system to use read-only replicas, scaling up more pods. But using the R/W connection when required.
Also see: https://redhat.atlassian.net/browse/TC-3439
Summary by Sourcery
Introduce separate read-write and read-only database connection wrappers and wire the application to use them across modules, enabling optional use of a dedicated read-only database while enforcing read-only access at the transaction level.
New Features:
Enhancements:
Documentation:
Tests:
Chores: