New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
sqldb: new package supporting sql backends #7343
Conversation
041c7e3
to
de45917
Compare
Makefile
Outdated
migrate -path sqldb/sqlc/migrations -database $(LND_DB_CONNECTIONSTRING) -verbose up | ||
|
||
migrate-down: $(MIGRATE_BIN) | ||
migrate -path sqldb/sqlc/migrations -database $(LND_DB_CONNECTIONSTRING) -verbose down 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you saw this comment already, but there's a gotcha with golang-migrate: #7251 (comment) / #6176 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is totally on point, thanks for sharing
d49fab0
to
e6eff7c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool to see what's needed to operate on two different backends, great work 🚀. Do you have plans to unify the backend creation with what's present in the kvdb package? Will also take a look at the follow-up PRs to build more context.
// NewTestPgFixture constructs a new TestPgFixture starting up a docker | ||
// container running Postgres 11. The started container will expire in after | ||
// the passed duration. | ||
func NewTestPgFixture(t *testing.T, expiry time.Duration) *TestPgFixture { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is already some fixture functionality in the kvdb_postgres
, perhaps it can be reused?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using the dockertest vs embedded postgres has some advantages. We can always extend the docker based one with more backends if needed and also can easily change versions at test time.
// system. | ||
// | ||
// First, we'll need to open up a new migration instance for | ||
// our current target database: sqlite. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: postgres
57cb5eb
to
ddd9560
Compare
5b0c990
to
8f47937
Compare
Concept Ack |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks very good to me, just a few comments/questions added. 🥇
// NewTestPgFixture constructs a new TestPgFixture starting up a docker | ||
// container running Postgres 11. The started container will expire in after | ||
// the passed duration. | ||
func NewTestPgFixture(t *testing.T, expiry time.Duration) *TestPgFixture { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using the dockertest vs embedded postgres has some advantages. We can always extend the docker based one with more backends if needed and also can easily change versions at test time.
func parseSqliteError(sqliteErr *sqlite.Error) error { | ||
switch sqliteErr.Code() { | ||
// Handle unique constraint violation error. | ||
case sqlite3.SQLITE_CONSTRAINT_UNIQUE: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if there's added value from mapping the unique constraint error at all? Apart from logging of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, we ended up doing this in the tap repo so we could get better logging from the DB, otherwise it's some like single integer return error from sqlite
.
defaultMaxConns = 25 | ||
|
||
// connIdleLifetime is the amount of time a connection can be idle. | ||
connIdleLifetime = 5 * time.Minute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this result in the connection pool shrinking? If so will the pool be repopulated automatically?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC, this lets a connection be idle for 5 mins at a time before being garbage colelcted. These "connections" re really just in memory structs (for sqlite) managed by the database/sql
package. FWIW, these values haven't been tuned a ton other than things like some of the pragma directives we realized we needed.
value: "full", | ||
}, | ||
{ | ||
// This is used to ensure proper durability for users |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is true, iirc mac users will suffer greatly when it comes to performance. Some itests may even fail too. I think the mac philosophy is that writes are durable as long as there's battery remaining and even then the OS is capable to putting the system to sleep when needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW this is only active on mac. It is the case that this dramatically improves perf if removed. IIRC, for sqlite
elsewhere in the project, we move these directives up one level and allow users to override them.
Can be rebased now! |
// struct. | ||
func (s *BaseDB) BeginTx(ctx context.Context, opts TxOptions) (*sql.Tx, error) { | ||
sqlOptions := sql.TxOptions{ | ||
ReadOnly: opts.ReadOnly(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also inherit the strongest serializability guarntees we can here as well: https://github.com/lightninglabs/taproot-assets/blob/main/tapdb/interfaces.go#L251
Note that this only actually matters for postgres.
We'll also want to revisit that mutex we added as well, as with this guarantee, we ensure that all transactions are serializable, and they're bailed if not. Hence the need for that retry logic I linked above.
func parseSqliteError(sqliteErr *sqlite.Error) error { | ||
switch sqliteErr.Code() { | ||
// Handle unique constraint violation error. | ||
case sqlite3.SQLITE_CONSTRAINT_UNIQUE: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, we ended up doing this in the tap repo so we could get better logging from the DB, otherwise it's some like single integer return error from sqlite
.
defaultMaxConns = 25 | ||
|
||
// connIdleLifetime is the amount of time a connection can be idle. | ||
connIdleLifetime = 5 * time.Minute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC, this lets a connection be idle for 5 mins at a time before being garbage colelcted. These "connections" re really just in memory structs (for sqlite) managed by the database/sql
package. FWIW, these values haven't been tuned a ton other than things like some of the pragma directives we realized we needed.
value: "full", | ||
}, | ||
{ | ||
// This is used to ensure proper durability for users |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW this is only active on mac. It is the case that this dramatically improves perf if removed. IIRC, for sqlite
elsewhere in the project, we move these directives up one level and allow users to override them.
@positiveblue, remember to re-request review from reviewers when ready |
Failing with a linter error atm:
|
0062060
to
8fd28e1
Compare
This commit provides the scaffolding for using the new sql stores. The new interfaces, structs and methods are in sync with other projects like Taproot Assets. - Transactional Queries: the sqldb package defines the interfaces required to execute transactional queries to our storage interface. - Migration Files Embedded: the migration files are embedded into the binary. - Database Migrations: I kept the use of 'golang-migrate' to ensure our codebase remains in sync with the other projects, but can be changed. - Build Flags for Conditional DB Target: flexibility to specify our database target at compile-time based on the build flags in the same way we do with our kv stores. - Update modules: ran `go mod tidy`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🍨
// randRetryDelay returns a random retry delay between 0 and the configured max | ||
// delay. | ||
func (t *txExecutorOptions) randRetryDelay() time.Duration { | ||
return time.Duration(prand.Int63n(int64(t.retryDelay))) //nolint:gosec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing we want to fix in the repo this originated was to make this actually have a min back off, as is right now, it can use a rather small value (say 1 ns) which causes the retries to bleed out pretty quickly.
So I think we want to also have a min value here, then offset from that, 50 ms for a min value seems good I think? Then we can add a TODO here to circle back on doing proper exponential backoff as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or perhaps we can also try and do an exponential ramp up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work @positiveblue, LGTM 🥇
// randRetryDelay returns a random retry delay between 0 and the configured max | ||
// delay. | ||
func (t *txExecutorOptions) randRetryDelay() time.Duration { | ||
return time.Duration(prand.Int63n(int64(t.retryDelay))) //nolint:gosec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or perhaps we can also try and do an exponential ramp up?
|
||
// Before we try again, we'll wait with a random backoff based | ||
// on the retry delay. | ||
time.Sleep(retryDelay) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the retry delay can take seconds (in the extreme case) perhaps worth using a select and time.After()
channel instead so we can quickly return if the passed in context is canceled.
} | ||
|
||
if stat.IsDir() { | ||
return f, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/err/nil
} | ||
|
||
rawDB.SetMaxOpenConns(maxConns) | ||
rawDB.SetMaxIdleConns(maxConns) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it's worth setting the idle conns to something lower? Perhaps defaultMaxConns/2?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very interesting PR, design still over my head but getting grip on it. Had some nits regarding the comments apart from that consider my review as a novice in this field.
) | ||
|
||
// TxOptions represents a set of options one can use to control what type of | ||
// database transaction is created. Transaction can wither be read or write. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: wither=>either
// TransactionExecutor is a generic struct that abstracts away from the type of | ||
// query a type needs to run under a database transaction, and also the set of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: not sure if I understand the sentence correctly but shouldn't this be:
TransactionExecutor is a generic struct that abstracts away from the type of query which type needs ...
} | ||
|
||
// NewTestPgFixture constructs a new TestPgFixture starting up a docker | ||
// container running Postgres 11. The started container will expire in after |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/in after/after?
return newReplacerFile(f, t.replaces) | ||
} | ||
|
||
type replacerFile struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Should this type also have a comment ?
This is the second PR for #6288
Only the latest commit applies to this PR
The previous PR we added the sql schema and queries for invoices.
In this PR provide the scaffolding for using the new sql stores.
The new interfaces, structs and methods are in sync with other projects like Taproot Assets.
Transactional Queries: the sqldb package defines the interfaces required to execute transactional queries to our storage interface.
Embed Migration Files: the migration files are embedded into the binary.
Database Migrations: I kept the use of 'golang-migrate' to ensure our codebase remains in sync with the other projects, but it can be changed. (@joostjager provided some feedback about golang-migrate pitfalls and how sql-migrate can be a good subtitute)
Build Flags for Conditional DB Target: flexibility to specify our database target at compile-time based on the build flags in the same way we do with our kv stores.
SQL utils: generic functions used for marshal/unmarshal db data. <= I will fix the linter later
Update modules: ran
go mod tidy
.NOTE: the structure of this PR has changed so some of the old comments don't apply or apply to other PRs.