Skip to content

sqldb: new package supporting sql backends#7343

Merged
Roasbeef merged 2 commits into
lightningnetwork:masterfrom
positiveblue:sqldb
Jul 31, 2023
Merged

sqldb: new package supporting sql backends#7343
Roasbeef merged 2 commits into
lightningnetwork:masterfrom
positiveblue:sqldb

Conversation

@positiveblue
Copy link
Copy Markdown
Contributor

@positiveblue positiveblue commented Jan 19, 2023

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.

@positiveblue positiveblue force-pushed the sqldb branch 2 times, most recently from 041c7e3 to de45917 Compare January 19, 2023 17:51
Comment thread Makefile Outdated
Copy link
Copy Markdown
Contributor

@joostjager joostjager Jan 20, 2023

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)

Copy link
Copy Markdown
Contributor Author

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

@positiveblue positiveblue force-pushed the sqldb branch 2 times, most recently from d49fab0 to e6eff7c Compare January 24, 2023 14:35
@saubyk saubyk added invoices sql database Related to the database/storage of LND labels Jan 26, 2023
@saubyk saubyk added this to the v0.16.0 milestone Jan 26, 2023
@saubyk saubyk requested a review from bitromortac January 26, 2023 18:22
Copy link
Copy Markdown
Collaborator

@bitromortac bitromortac left a 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.

Comment thread sqlc.yaml Outdated
Comment thread sqlc.yaml Outdated
Comment thread sqldb/sqlc/migrations/000001_sequences.up.sql Outdated
Comment thread sqldb/postgres_fixture.go Outdated
Copy link
Copy Markdown
Collaborator

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?

Copy link
Copy Markdown
Collaborator

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.

Comment thread sqldb/test_sqlite.go Outdated
Comment thread lnrpc/autopilotrpc/autopilot.pb.go Outdated
Comment thread sqldb/postgres.go Outdated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: postgres

@saubyk
Copy link
Copy Markdown
Collaborator

saubyk commented Jun 13, 2023

Concept Ack

Copy link
Copy Markdown
Collaborator

@bhandras bhandras left a 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. 🥇

Comment thread sqldb/postgres_fixture.go Outdated
Copy link
Copy Markdown
Collaborator

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.

Comment thread sqldb/sqlerrors.go
func parseSqliteError(sqliteErr *sqlite.Error) error {
switch sqliteErr.Code() {
// Handle unique constraint violation error.
case sqlite3.SQLITE_CONSTRAINT_UNIQUE:
Copy link
Copy Markdown
Collaborator

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.

Copy link
Copy Markdown
Member

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.

Comment thread sqldb/sqlite.go
defaultMaxConns = 25

// connIdleLifetime is the amount of time a connection can be idle.
connIdleLifetime = 5 * time.Minute
Copy link
Copy Markdown
Collaborator

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?

Copy link
Copy Markdown
Member

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.

Comment thread sqldb/sqlite.go
value: "full",
},
{
// This is used to ensure proper durability for users
Copy link
Copy Markdown
Collaborator

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.

Copy link
Copy Markdown
Member

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.

@Roasbeef
Copy link
Copy Markdown
Member

Can be rebased now!

Comment thread sqldb/interfaces.go Outdated
Comment thread sqldb/interfaces.go
// struct.
func (s *BaseDB) BeginTx(ctx context.Context, opts TxOptions) (*sql.Tx, error) {
sqlOptions := sql.TxOptions{
ReadOnly: opts.ReadOnly(),
Copy link
Copy Markdown
Member

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.

Comment thread sqldb/sqlerrors.go
func parseSqliteError(sqliteErr *sqlite.Error) error {
switch sqliteErr.Code() {
// Handle unique constraint violation error.
case sqlite3.SQLITE_CONSTRAINT_UNIQUE:
Copy link
Copy Markdown
Member

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.

Comment thread sqldb/sqlite.go
defaultMaxConns = 25

// connIdleLifetime is the amount of time a connection can be idle.
connIdleLifetime = 5 * time.Minute
Copy link
Copy Markdown
Member

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.

Comment thread sqldb/sqlite.go
value: "full",
},
{
// This is used to ensure proper durability for users
Copy link
Copy Markdown
Member

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.

@lightninglabs-deploy
Copy link
Copy Markdown
Collaborator

@positiveblue, remember to re-request review from reviewers when ready

@Roasbeef
Copy link
Copy Markdown
Member

Failing with a linter error atm:

no changes added to commit (use "git add" and/or "git commit -a")
make: *** [Makefile:239: fmt-check] Error 1
diff --git a/sqldb/postgres.go b/sqldb/postgres.go
index 72f31c9e7..bc[34](https://github.com/lightningnetwork/lnd/actions/runs/5597978693/job/15163270504?pr=7343#step:4:35)ecb43 100644
--- a/sqldb/postgres.go
+++ b/sqldb/postgres.go
@@ -7,7 +7,6 @@ import (
 	"time"
 
 	postgres_migrate "github.com/golang-migrate/migrate/v4/database/postgres"
-	// Read migrations from files.
 	_ "github.com/golang-migrate/migrate/v4/source/file"
 	"github.com/lightningnetwork/lnd/sqldb/sqlc"
 	"github.com/stretchr/testify/require"
diff --git a/sqldb/postgres_fixture.go b/sqldb/postgres_fixture.go
index f8dc2dd3d..becf3007d 1006[44](https://github.com/lightningnetwork/lnd/actions/runs/5597978693/job/15163270504?pr=7343#step:4:45)
--- a/sqldb/postgres_fixture.go
+++ b/sqldb/postgres_fixture.go
@@ -9,7 +9,6 @@ import (
 	"testing"
 	"time"
 
-	// Import the postgres driver.
 	_ "github.com/lib/pq"
 	"github.com/ory/dockertest/v3"
 	"github.com/ory/dockertest/v3/docker"

Comment thread sqldb/interfaces.go Outdated
@positiveblue positiveblue force-pushed the sqldb branch 2 times, most recently from 0062060 to 8fd28e1 Compare July 27, 2023 09:45
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`.
Copy link
Copy Markdown
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🍨

Comment thread sqldb/interfaces.go
// 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
Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Collaborator

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?

Copy link
Copy Markdown
Collaborator

@bhandras bhandras left a 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 🥇

Comment thread sqldb/interfaces.go
// 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
Copy link
Copy Markdown
Collaborator

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?

Comment thread sqldb/interfaces.go

// Before we try again, we'll wait with a random backoff based
// on the retry delay.
time.Sleep(retryDelay)
Copy link
Copy Markdown
Collaborator

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.

Comment thread sqldb/migrations.go
}

if stat.IsDir() {
return f, err
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s/err/nil

Comment thread sqldb/postgres.go
}

rawDB.SetMaxOpenConns(maxConns)
rawDB.SetMaxIdleConns(maxConns)
Copy link
Copy Markdown
Collaborator

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?

Copy link
Copy Markdown
Collaborator

@ziggie1984 ziggie1984 left a 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.

Comment thread sqldb/interfaces.go
)

// 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: wither=>either

Comment thread sqldb/interfaces.go
Comment on lines +73 to +129
// 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
Copy link
Copy Markdown
Collaborator

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 ...

Comment thread sqldb/postgres_fixture.go
}

// NewTestPgFixture constructs a new TestPgFixture starting up a docker
// container running Postgres 11. The started container will expire in after
Copy link
Copy Markdown
Collaborator

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?

Comment thread sqldb/migrations.go
return newReplacerFile(f, t.replaces)
}

type replacerFile struct {
Copy link
Copy Markdown
Collaborator

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 ?

@Roasbeef Roasbeef merged commit 1871970 into lightningnetwork:master Jul 31, 2023
@saubyk saubyk moved this from High Priority to Merged in PR Review Priority May 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

database Related to the database/storage of LND invoices sql

Projects

Status: Merged
Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

8 participants