Create and initialize the database if it does not exist#55
Create and initialize the database if it does not exist#55tnull merged 4 commits intolightningdevkit:mainfrom
Conversation
|
👋 Thanks for assigning @tnull as a reviewer! |
tnull
left a comment
There was a problem hiding this comment.
Took a first look and left some higher-level comments. Have yet to review the actual database statements and operations.
|
A possible todo I thought of: right now our |
rust/impls/src/postgres_store.rs
Outdated
| pub async fn new(postgres_endpoint: &str, db_name: &str, init_db: bool) -> Result<Self, Error> { | ||
| if init_db { | ||
| tokio::time::timeout( | ||
| tokio::time::Duration::from_secs(3), |
There was a problem hiding this comment.
Can we extract the timeout value(s) into appropriately named consts?
rust/impls/src/postgres_store.rs
Outdated
| Ok(()) | ||
| } | ||
|
|
||
| async fn check_health(&self) -> Result<(), Error> { |
There was a problem hiding this comment.
Do we really need this? I think we should be able to trust that the CREATE TABLE statement does the right thing if it doesn't error out?
There was a problem hiding this comment.
This mostly useful if the user does not set --init-db; in that case we want to make sure we can establish a connection to the database as part of startup checks before entering the main loop ?
If this passes, then we can log something like "Connected to PostgreSQL backend" in main as you suggested.
rust/server/src/main.rs
Outdated
| eprintln!("Usage: {} <config-file-path> [--init-db]", args[0]); | ||
| std::process::exit(1); | ||
| } | ||
| let init_db = args.iter().any(|arg| arg == "--init-db"); |
There was a problem hiding this comment.
Why is this an optional flag to begin with?
There was a problem hiding this comment.
- When the flag is not set, I want the startup sequence to abort if the database is not present.
- When the flag is set, I want the startup sequence to create the database if it is not present.
There was a problem hiding this comment.
Right, just wondering when we'd ever want the first case?
If the intention is that we can initiate a VSS server instance even if we don't have / can establish a connection to the Postgres backend (which I don't fully buy), then we'd need to take additional steps. AFAIU, Pool::build(), which we call ~just after the init_db block would just as well fail if we can't connect to the pool:
"The Pool will not be returned until it has established its configured minimum number of connections, or it times out." (https://docs.rs/bb8/0.9.0/bb8/struct.Builder.html#method.build)
There was a problem hiding this comment.
Right, just wondering when we'd ever want the first case?
If the user does not intend to initialize a new database (which is most cases), I don't think we should silently create a new database if we don't find the one we are looking for. Instead, I think we should complain loudly, and make sure the user actually intends to create a new one.
If the intention is that we can initiate a VSS server instance even if we don't have / can establish a connection to the Postgres backend (which I don't fully buy), then we'd need to take additional steps. AFAIU, Pool::build(), which we call ~just after the init_db block would just as well fail if we can't connect to the pool:
Actually Pool::build returns Ok(()) even if it cannot establish a connection to the Postgres backend - see #52 (comment) . On the main branch, we then enter the main loop even though we have established no connections to the Postgres backend.
There was a problem hiding this comment.
If the user does not intend to initialize a new database (which is most cases), I don't think we should silently create a new database if we don't find the one we are looking for. Instead, I think we should complain loudly, and make sure the user actually intends to create a new one.
Hmm, I don't know. A binary that needs a special flag to init just on first start is a bit weird tbh. I can't think of another daemon that works that way.
Actually
Pool::buildreturnsOk(())even if it cannot establish a connection to the Postgres backend - see #52 (comment) . On the main branch, we then enter the main loop even though we have established no connections to the Postgres backend.
Huh, this API is very unexpected then. You'd expect it to return a TimedOut error if the connection attempts failed, just as get does. Might even be good to add a comment there in our code.
There was a problem hiding this comment.
Huh, this API is very unexpected then. You'd expect it to return a
TimedOuterror if the connection attempts failed, just asgetdoes. Might even be good to add a comment there in our code.
Sounds good will add the comment. Agreed this is unexpected.
There was a problem hiding this comment.
Hmm, I don't know. A binary that needs a special flag to init just on first start is a bit weird tbh. I can't think of another daemon that works that way.
I agree it is weird. FWIW, CLN, LDK-node automatically generates a new wallet, but LND requires you to explicitly run lncli createwallet instead of lncli unlock (lncli unlock aborts startup if the wallet does not exist).
If we always create a new database if it does not exist, when a user misconfigures the DB connection string to an existing database, we'll create a new database and complete startup, when instead we should(?) have aborted startup. See #52 (comment)
cc @TheBlueMatt
There was a problem hiding this comment.
I don't have a super strong feeling. I've seen it done both ways, though it is generally the case for binaries like this that they'll auto-create the table by default, I can see why that might be surprising.
rust/server/src/main.rs
Outdated
| .await | ||
| .unwrap(), | ||
| ); | ||
| println!("Loaded postgres!"); |
There was a problem hiding this comment.
If I read that message I'd be a bit confused what's meant exactly. Maybe "Connected to PostgreSQL backend with address .."?
There was a problem hiding this comment.
Yes that's clearer I agree thank you
rust/server/src/main.rs
Outdated
| println!("Loaded postgres!"); | ||
| let rest_svc_listener = | ||
| TcpListener::bind(&addr).await.expect("Failed to bind listening port"); | ||
| println!("Bound to {}", addr); |
There was a problem hiding this comment.
Maybe "Listening for incoming connections on .."
tnull
left a comment
There was a problem hiding this comment.
Let me know when this is ready for another round of review.
Thanks for the review @tnull I've responded to your comments above would be good to see your responses before I write more code :) |
rust/impls/src/postgres_store.rs
Outdated
| const DB_INIT_CMD: &str = "CREATE DATABASE"; | ||
| const TABLE_CHECK_STMT: &str = "SELECT 1 FROM vss_db WHERE false"; | ||
| const TABLE_INIT_STMT: &str = " | ||
| CREATE TABLE IF NOT EXISTS vss_db ( |
There was a problem hiding this comment.
In addition to the main table itself, we really need a "config" table that at least just stores the version of the schema we're currently using. That way we can easily check that the schema in use is compatible with this version and, in the future, we can upgrade the schema as needed.
There was a problem hiding this comment.
I think in Postgres migrations are usually done by sequentially applying the corresponding XXX_YYY.sql files. I agree it's probably good to track what version was last applied in a separate schema_version table or similar.
However, this probably also highlights that it would make sense to move the SQL statements to a dedicated migrations.rs, and rename TABLE_INIT_STMT in accordance to the .sql file to const V0_CREATE_VSS_DB, essentially to prepare for the eventual V1_.., V2_, .. migrations to follow.
FWIW, we could even consider to read in the sql files on startup. If we don't want to do that, we could consider dropping the sql files and just have the migrations.rs to avoid the risk of having them get out-of-sync at some point.
7396c56 to
eace62d
Compare
|
🔔 1st Reminder Hey @tnull! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @tnull! This PR has been waiting for your review. |
tnull
left a comment
There was a problem hiding this comment.
Some minor comments, but basically LGTM
rust/impls/src/postgres_store.rs
Outdated
| const UPDATE_VERSION_STMT: &str = "UPDATE vss_db_version SET db_version=$1;"; | ||
| const LOG_MIGRATION_STMT: &str = "INSERT INTO vss_db_upgrades VALUES($1);"; | ||
|
|
||
| const MIGRATIONS: &[&str] = &[ |
There was a problem hiding this comment.
Mind adding a comment here that these are meant to be append-only (i.e., should never been changed in-place) and need to be run in order, and only the needed migration should be run?
Also, would it make sense to split out the migrations and potential test code into a migrations.rs?
There was a problem hiding this comment.
I preferred to move just the constants to migration.rs to keep migration.rs as lean as possible - that file should never change. Let me know how this sounds.
rust/impls/src/postgres_store.rs
Outdated
| const MIGRATIONS: &[&str] = &[ | ||
| "CREATE TABLE vss_db_version (db_version INTEGER);", | ||
| "INSERT INTO vss_db_version VALUES(1);", | ||
| "CREATE TABLE vss_db_upgrades (upgrade_from INTEGER);", |
There was a problem hiding this comment.
Could you expand on what would be stored in the vss_db_upgrades table? Why do we need it in addition to vss_db_version?
rust/impls/src/postgres_store.rs
Outdated
| Ok(postgres_backend) | ||
| } | ||
|
|
||
| async fn migrate_vss_database(&self) -> Result<(), Error> { |
There was a problem hiding this comment.
Would be cool to get some test coverage for this migration logic.
There was a problem hiding this comment.
Thank you see below
tnull
left a comment
There was a problem hiding this comment.
LGTM, please squash fixups.
Also implement a versioning and migration scheme for future updates to the schema. It is an adaptation of the scheme used in CLN.
|
Squashed fixups with no other changes |
and