Skip to content
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

Merged
merged 2 commits into from Jul 31, 2023

Conversation

positiveblue
Copy link
Collaborator

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

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
Copy link
Collaborator

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

@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
Copy link
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.

sqlc.yaml Outdated Show resolved Hide resolved
sqlc.yaml Outdated Show resolved Hide resolved
sqldb/sqlc/migrations/000001_sequences.up.sql Outdated Show resolved Hide resolved
// 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 {
Copy link
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
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.

sqldb/test_sqlite.go Outdated Show resolved Hide resolved
lnrpc/autopilotrpc/autopilot.pb.go Outdated Show resolved Hide resolved
// system.
//
// First, we'll need to open up a new migration instance for
// our current target database: sqlite.
Copy link
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
Collaborator

saubyk commented Jun 13, 2023

Concept Ack

Copy link
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. 🥇

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

func parseSqliteError(sqliteErr *sqlite.Error) error {
switch sqliteErr.Code() {
// Handle unique constraint violation error.
case sqlite3.SQLITE_CONSTRAINT_UNIQUE:
Copy link
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
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.

defaultMaxConns = 25

// connIdleLifetime is the amount of time a connection can be idle.
connIdleLifetime = 5 * time.Minute
Copy link
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
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.

value: "full",
},
{
// This is used to ensure proper durability for users
Copy link
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
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
Member

Can be rebased now!

sqldb/interfaces.go Outdated Show resolved Hide resolved
// struct.
func (s *BaseDB) BeginTx(ctx context.Context, opts TxOptions) (*sql.Tx, error) {
sqlOptions := sql.TxOptions{
ReadOnly: opts.ReadOnly(),
Copy link
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.

func parseSqliteError(sqliteErr *sqlite.Error) error {
switch sqliteErr.Code() {
// Handle unique constraint violation error.
case sqlite3.SQLITE_CONSTRAINT_UNIQUE:
Copy link
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.

defaultMaxConns = 25

// connIdleLifetime is the amount of time a connection can be idle.
connIdleLifetime = 5 * time.Minute
Copy link
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.

value: "full",
},
{
// This is used to ensure proper durability for users
Copy link
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

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

@Roasbeef
Copy link
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"

sqldb/interfaces.go Outdated Show resolved Hide resolved
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
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 🍨

// 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
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
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
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 🥇

// 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
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?


// Before we try again, we'll wait with a random backoff based
// on the retry delay.
time.Sleep(retryDelay)
Copy link
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.

}

if stat.IsDir() {
return f, err
Copy link
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

}

rawDB.SetMaxOpenConns(maxConns)
rawDB.SetMaxIdleConns(maxConns)
Copy link
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
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.

)

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

}

// NewTestPgFixture constructs a new TestPgFixture starting up a docker
// container running Postgres 11. The started container will expire in after
Copy link
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?

return newReplacerFile(f, t.replaces)
}

type replacerFile struct {
Copy link
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
20 of 24 checks passed
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: High Priority
Status: Done
Development

Successfully merging this pull request may close these issues.

None yet

8 participants