Skip to content

[sql-32] accounts: add migration code from kvdb to SQL #1047

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

ViktorTigerstrom
Copy link
Contributor

This PR introduces the migration logic for transitioning the accounts store from kvdb to SQL.

Note that as of this PR, the migration is not yet triggered by any production code, i.e. only tests execute the migration logic.

Part of #917

@ViktorTigerstrom ViktorTigerstrom added the no-changelog This PR is does not require a release notes entry label Apr 22, 2025
Copy link
Member

@ellemouton ellemouton left a comment

Choose a reason for hiding this comment

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

Soooooo excited to see this come through! I couldnt help but take a look even though review not requested - just leaving some drive by comments in the mean time :)

I'll wait till review requested before looking again :)

Comment on lines 329 to 335

t.Run("Postgres", func(t *testing.T) {
migrationTest(t, kvStore, testClock, false)
})

t.Run("SQLite", func(t *testing.T) {
migrationTest(t, kvStore, testClock, true)
Copy link
Member

Choose a reason for hiding this comment

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

in Lit, we've gone with the build flag approach. So can just use the existing NewTestDB methods no? and then if bbolt backend, just skip the test ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated as discussed offline :)

@@ -732,6 +733,79 @@ func (s *SQLStore) StoreLastIndexes(ctx context.Context, addIndex,
})
}

func makeInsertAccountParams(account *OffChainBalanceAccount) (
Copy link
Member

Choose a reason for hiding this comment

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

let's keep the migration code in the same file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, yeah agree :)! Had it here previously to mimic lnd's migration implementation.

Comment on lines 30 to 47
pgFixture := db.NewTestPgFixture(
t, db.DefaultPostgresFixtureLifetime, true,
)
t.Cleanup(func() {
pgFixture.TearDown(t)
})

makeSQLDB := func(t *testing.T, clock *clock.TestClock,
sqlite bool) (*SQLStore, *db.TransactionExecutor[SQLQueries]) {

var store *SQLStore

if sqlite {
store = NewSQLStore(db.NewTestSqliteDB(t).BaseDB, clock)
} else {
store = NewSQLStore(
db.NewTestPostgresDB(t).BaseDB, clock,
)
Copy link
Member

Choose a reason for hiding this comment

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

see other comment about using the existing infrastructure in LiT that uses build flags instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, updated!

Comment on lines 96 to 92
// If the db doesn't have any indices, we can't compare
// them.
Copy link
Member

Choose a reason for hiding this comment

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

if the kvstore didnt populate during test set up though, then we will silently exit here. Rather have a test param like "expectLastIndex" so that we know for sure if we are getting the right result here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah good catch, I actually forgot to store the indexes when i added invoices, so that caught that 🚀!

@ViktorTigerstrom ViktorTigerstrom force-pushed the 2025-04-migrate-accounts branch from 3072e48 to b4cffd8 Compare April 24, 2025 09:12
Comment on lines 327 to 336
{
"randomized accounts",
true,
randomizeAccounts,
},
{
"rapid randomized accounts",
true,
rapidRandomizeAccounts,
},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added 2 tests here currently, where one uses rapid for the randomisation, and one uses the normal "golang.org/x/exp/rand" testing semi-randomisation.

The drawback with the rapid test, is that we can't really generate too many invoices and and payments per account, as you can't really limit the amount of times the rapid.Check runs in golang (i.e. the number of accounts we populate the db with), without limiting the amount of runs the Check function would run for the entire litd project. I.e. if we limited it for this test, that would impact any other tests in the future that uses the rapid.Check function. There is an open issue for this in the rapid lib, see:
flyingmutant/rapid#38

When running the migration locally, the test migration execution time really goes up if you have a lot of accounts with a lot of payments and invoices, and therefore I opted to not add such a test to not make the make unit execution time take too long locally.

Therefore, I also added a normal test that doesn't use rapid for randomization, so that test adds up to 1000 invoices and payments.

In the end though , I can delete one of the tests if you prefer any test over the other :)!

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I prefer randomizeAccounts since it's more concise and performant, great you tested out both approaches 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool, thanks for the input! Would he interested to hear Elle's opinion here as well :)

Copy link
Contributor

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

Awesome work 🚀! Will certainly need another pass, since I haven't reviewed many migration PRs

AccountID: sqlID,
Hash: hash[:],
Status: int16(entry.Status),
FullAmountMsat: int64(entry.FullAmount),
Copy link
Contributor

Choose a reason for hiding this comment

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

an overflow is very unrealistic here, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I think we have to make a choice here, and for migrations in general:
We can either:

  1. Assume that since this is a migration, the data we are migrating must have already been sanity checked and rule checked during the initial insertion into the kvdb, and therefore errors like that shouldn't be possible to occur during the migration.
  2. We sanity check and check the rules again for all data during the migration.

I have opted for option 1 here and for upcoming migrations, as I do think that should be the case, and we'd only let the migrations use more resources during the migration if we chose option 2 instead.

But I'd very interested to hear from both of you if you think we should instead go with option 2 here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I'm unsure, maybe we should be extra careful with amounts, but we can leave it as is.

Copy link
Member

Choose a reason for hiding this comment

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

I think we are already indirectly checking though: if we went with option 2, we'd check here and error out if it did overflow - but if we go with option 2, we will just fail later on when we do the reflect.DeapEqual check.

so i think for simplicity we dont need to add the redundant check

Comment on lines 327 to 336
{
"randomized accounts",
true,
randomizeAccounts,
},
{
"rapid randomized accounts",
true,
rapidRandomizeAccounts,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I prefer randomizeAccounts since it's more concise and performant, great you tested out both approaches 👍

@ViktorTigerstrom ViktorTigerstrom force-pushed the 2025-04-migrate-accounts branch from 8cd1631 to 7ccdd7f Compare May 2, 2025 08:44
In preparation for upcoming migration tests from a kvdb to an SQL store,
this commit updates the NewTestDB function to return the Store interface
rather than a concrete store implementation.

This change ensures that migration tests can call NewTestDB under any
build tag while receiving a consistent return type.
This commit introduces the migration logic for transitioning the
accounts store from kvdb to SQL.

Note that as of this commit, the migration is not yet triggered by any
production code, i.e. only tests execute the migration logic.
@ViktorTigerstrom ViktorTigerstrom force-pushed the 2025-04-migrate-accounts branch from 7ccdd7f to 26b6809 Compare May 2, 2025 08:48
@ViktorTigerstrom
Copy link
Contributor Author

Thanks for the reviews! I've addressed feedback and left comments with the second last push. In the last push, I rebased on master to address a merge conflict.

Copy link
Contributor

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

LGTM 🔥🚀

makeSQLDB := func(t *testing.T, clock *clock.TestClock) (*SQLStore,
*db.TransactionExecutor[SQLQueries]) {

testDBStore := NewTestDB(t, clock)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: call Close after the test completed?

},
},
{
"account with updated expiry",
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess the intention here is to test the UpdatedAt field, right? If so that would be a better name for the test

},
},
{
"account with updated balance",
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this test needed? it seems like it only tests CreditAccount/DebitAccount in addition. Also, maybe we can remove the HasExpired checks where applicable, as this adds a bit of noise?

Comment on lines +383 to +386
// randomize balance from 1,000 to 100,000,000
balance := lnwire.MilliSatoshi(
rand.Int63n(100000000-1000) + 1000,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it important that we have a lower bound in these tests? Nano-nit: the upper bound is not included, just to be aware

rand.Int63n(100000000-1000) + 1000,
)

// randomize expiry from 10 to 10,000 minutes
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: comments should be formatted as sentences

// invoices and payments. The invoices and payments are also generated with
// random hashes and amounts.
func makeAccountGen() *rapid.Generator[account] {
return rapid.Custom[account](func(t *rapid.T) account {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: the type parameter can be omitted, same below in randomHash

AccountID: sqlID,
Hash: hash[:],
Status: int16(entry.Status),
FullAmountMsat: int64(entry.FullAmount),
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I'm unsure, maybe we should be extra careful with amounts, but we can leave it as is.

@ViktorTigerstrom
Copy link
Contributor Author

Thanks for the review @bitromortac! Will address feedback after next review round 🚀!

Copy link
Member

@ellemouton ellemouton left a comment

Choose a reason for hiding this comment

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

looking really great!

Note that as of this PR, the migration is not yet triggered by any production code, i.e. only tests execute the migration logic.

so reviewing this PR made me realise we should perhaps flip the order of things a bit: basically, i still want to be able to test this PR properly (ie, i want to run master branch - add some accounts, then switch to this PR and see that the accounts get migrated properly).

So this means that under the dev tag, we should first just add an option to point to an existing kvdb folder where we can migrate any existing accounts from (if we spin up in sql mode). Note: this probably means adding a set-up PR before we merge this one. But yeah - i think well worth it so that we can test these individually


err := migrateAccountsToSQL(ctx, kvStore, tx)
if err != nil {
return err
Copy link
Member

Choose a reason for hiding this comment

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

nit: wrap with formatted string so we can tell if error is coming from this migration or the next one

total++

migratedAccountID, err := migrateSingleAccountToSQL(
ctx, tx, kvAccounts[i],
Copy link
Member

Choose a reason for hiding this comment

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

not a big deal but there is mixed use of kvAccount and kvAcounts[i] here. would prefer if we could just stick to 1 just for consistency.

return err
}

total := 0
Copy link
Member

Choose a reason for hiding this comment

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

is total ever not just len(kvAccounts)?

populateDB func(t *testing.T, kvStore *BoltStore)
}{
{
"empty",
Copy link
Member

Choose a reason for hiding this comment

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

let's always use named fields 🙏

Copy link
Member

Choose a reason for hiding this comment

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

reason being: right now, it's hard to do a quick check of things like "are there tests that cover expectedLastIndex=false" for example. if they are named, then it is very easy to check

}
}

accountId, err := account.ID.ToInt64()
Copy link
Member

Choose a reason for hiding this comment

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

let's call it "accountAlias"

Copy link
Member

Choose a reason for hiding this comment

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

or legacyID

}

for _, test := range tests {
tc := test
Copy link
Member

Choose a reason for hiding this comment

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

I thiiiink we dont need to do this anymore right?

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

var kvStore *BoltStore
Copy link
Member

Choose a reason for hiding this comment

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

not needed?

require.NoError(t, kvStore.db.Close())
})

migrationTest(t, kvStore, testClock, tc.expectLastIndex)
Copy link
Member

Choose a reason for hiding this comment

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

not loving the flow here cause some of the setup is done here and the rest is done in migrationTest.

Thinking something like this instead:

	// Create a new kvdb store to populate with test data.
			kvStore, err := NewBoltStore(
				t.TempDir(), DBFilename, clock,
			)
			require.NoError(t, err)
			t.Cleanup(func() {
				require.NoError(t, kvStore.db.Close())
			})

			// Populate the kv store.
			test.populateDB(t, kvStore)

			// Create the SQL store that we will migrate the data
			// to.
			sqlStore, txEx := makeSQLDB(t)

			// Perform the migration.
			var opts sqldb.MigrationTxOptions
			err = txEx.ExecTx(ctx, &opts, func(tx SQLQueries) error {
				return MigrateAccountStoreToSQL(
					ctx, kvStore, tx,
				)
			})
			require.NoError(t, err)
			
			// Assert migration results.
			assertMigrationResults(
				t, kvStore, sqlStore, test.expectLastIndex,
			)

Copy link
Member

Choose a reason for hiding this comment

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

notice how the order of operations mimics that of what would actually happen on a real node.

}
}

// RapidRandomizeAccounts is a rapid test that generates randomized
Copy link
Member

Choose a reason for hiding this comment

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

nit: function name mismatch

@@ -346,3 +360,227 @@ func TestAccountStoreMigration(t *testing.T) {
})
}
}

Copy link
Member

Choose a reason for hiding this comment

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

nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-changelog This PR is does not require a release notes entry
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants