graph/db+sqldb: make policy migration idempotent

Finally, we make the channel-policy part of the SQL migration idempotent
by adding a migration-only policy insert query which will not error out
if the policy already exists and does not have a timestamp that is newer
than the existing records timestamp. To keep the commit simple, a
insertChanEdgePolicyMig function is added which is basically identical
to the updateChanEdgePolicy function except for the fact that it uses
the newly added query. In the next commit, it will be simplified even
more.
This commit is contained in:
Elle Mouton committed 2025-09-01 08:06:12 +02:00
1 parent 8736fcafa8
commit 22bf88e900
6 files changed
+221 -23

No files matched your search

+100 -1
View File
@@ -454,7 +454,7 @@ func migrateChannelsAndPolicies(ctx context.Context, cfg *SQLStoreConfig,
policyCount++
_, _, _, err := updateChanEdgePolicy(ctx, sqlDB, policy)
_, _, _, err := insertChanEdgePolicyMig(ctx, sqlDB, policy)
if err != nil {
return fmt.Errorf("could not migrate channel "+
"policy %d: %w", policy.ChannelID, err)
@@ -1560,3 +1560,102 @@ func insertChannelMig(ctx context.Context, db SQLQueries,
node2ID: node2DBID,
}, nil
}
// insertChanEdgePolicyMig inserts the channel policy info we have stored for
// a channel we already know of. This is used during the SQL migration
// process to insert channel policies.
//
// TODO(elle): update this function to be more performant in the migration
// setting. For the sake of keeping the commit that introduced this function
// simple, this is for now mostly the same as updateChanEdgePolicy.
func insertChanEdgePolicyMig(ctx context.Context, tx SQLQueries,
edge *models.ChannelEdgePolicy) (route.Vertex, route.Vertex, bool,
error) {
var (
node1Pub, node2Pub route.Vertex
isNode1 bool
chanIDB = channelIDToBytes(edge.ChannelID)
)
// Check that this edge policy refers to a channel that we already
// know of. We do this explicitly so that we can return the appropriate
// ErrEdgeNotFound error if the channel doesn't exist, rather than
// abort the transaction which would abort the entire batch.
dbChan, err := tx.GetChannelAndNodesBySCID(
ctx, sqlc.GetChannelAndNodesBySCIDParams{
Scid: chanIDB,
Version: int16(ProtocolV1),
},
)
if errors.Is(err, sql.ErrNoRows) {
return node1Pub, node2Pub, false, ErrEdgeNotFound
} else if err != nil {
return node1Pub, node2Pub, false, fmt.Errorf("unable to "+
"fetch channel(%v): %w", edge.ChannelID, err)
}
copy(node1Pub[:], dbChan.Node1PubKey)
copy(node2Pub[:], dbChan.Node2PubKey)
// Figure out which node this edge is from.
isNode1 = edge.ChannelFlags&lnwire.ChanUpdateDirection == 0
nodeID := dbChan.NodeID1
if !isNode1 {
nodeID = dbChan.NodeID2
}
var (
inboundBase sql.NullInt64
inboundRate sql.NullInt64
)
edge.InboundFee.WhenSome(func(fee lnwire.Fee) {
inboundRate = sqldb.SQLInt64(fee.FeeRate)
inboundBase = sqldb.SQLInt64(fee.BaseFee)
})
id, err := tx.InsertEdgePolicyMig(ctx, sqlc.InsertEdgePolicyMigParams{
Version: int16(ProtocolV1),
ChannelID: dbChan.ID,
NodeID: nodeID,
Timelock: int32(edge.TimeLockDelta),
FeePpm: int64(edge.FeeProportionalMillionths),
BaseFeeMsat: int64(edge.FeeBaseMSat),
MinHtlcMsat: int64(edge.MinHTLC),
LastUpdate: sqldb.SQLInt64(edge.LastUpdate.Unix()),
Disabled: sql.NullBool{
Valid: true,
Bool: edge.IsDisabled(),
},
MaxHtlcMsat: sql.NullInt64{
Valid: edge.MessageFlags.HasMaxHtlc(),
Int64: int64(edge.MaxHTLC),
},
MessageFlags: sqldb.SQLInt16(edge.MessageFlags),
ChannelFlags: sqldb.SQLInt16(edge.ChannelFlags),
InboundBaseFeeMsat: inboundBase,
InboundFeeRateMilliMsat: inboundRate,
Signature: edge.SigBytes,
})
if err != nil {
return node1Pub, node2Pub, isNode1,
fmt.Errorf("unable to upsert edge policy: %w", err)
}
// Convert the flat extra opaque data into a map of TLV types to
// values.
extra, err := marshalExtraOpaqueData(edge.ExtraOpaqueData)
if err != nil {
return node1Pub, node2Pub, false, fmt.Errorf("unable to "+
"marshal extra opaque data: %w", err)
}
// Update the channel policy's extra signed fields.
err = upsertChanPolicyExtraSignedFields(ctx, tx, id, extra)
if err != nil {
return node1Pub, node2Pub, false, fmt.Errorf("inserting chan "+
"policy extra TLVs: %w", err)
}
return node1Pub, node2Pub, isNode1, nil
}
+4 -22
View File
@@ -105,15 +105,6 @@ func TestMigrateGraphToSQL(t *testing.T) {
write func(t *testing.T, db *KVStore, object any)
objects []any
expGraphStats graphStats
// expNotRetrySafety is true if we expect an error to occur for
// the test if the migration is run twice. In other-words, if
// the specific case in question is currently not idempotent.
//
// NOTE: we want _all_ the cases here to be idempotent, so this
// is a temporary field which will be removed once we have
// properly made the migration retry-safe.
expNotRetrySafety bool
}{
{
name: "empty",
@@ -297,7 +288,6 @@ func TestMigrateGraphToSQL(t *testing.T) {
numChannels: 3,
numPolicies: 3,
},
expNotRetrySafety: true,
},
{
name: "prune log",
@@ -409,19 +399,11 @@ func TestMigrateGraphToSQL(t *testing.T) {
// Validate that the two databases are now in sync.
assertInSync(t, kvDB, sql, test.expGraphStats)
// NOTE: for now, not all the cases in the test are
// retry safe! The aim is to completely remove this
// field once we have made the migration retry-safe.
// The migration should be retry-safe, so running it
// again should not change the state of the databases.
err = MigrateGraphToSQL(ctx, sql.cfg, kvDB.db, sql.db)
if !test.expNotRetrySafety {
// The migration should be retry-safe, so
// running it again should not change the state
// of the databases.
require.NoError(t, err)
assertInSync(t, kvDB, sql, test.expGraphStats)
} else {
require.Error(t, err)
}
require.NoError(t, err)
assertInSync(t, kvDB, sql, test.expGraphStats)
})
}
}
+1
View File
@@ -164,6 +164,7 @@ type SQLQueries interface {
*/
InsertNodeMig(ctx context.Context, arg sqlc.InsertNodeMigParams) (int64, error)
InsertChannelMig(ctx context.Context, arg sqlc.InsertChannelMigParams) (int64, error)
InsertEdgePolicyMig(ctx context.Context, arg sqlc.InsertEdgePolicyMigParams) (int64, error)
}
// BatchedSQLQueries is a version of SQLQueries that's capable of batched