Skip to content

Commit c7452f3

Browse files
session: add migration code from kvdb to SQL
This commit introduces the migration logic for transitioning the sessions 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.
1 parent 6b7d00d commit c7452f3

File tree

2 files changed

+751
-0
lines changed

2 files changed

+751
-0
lines changed

session/sql_migration.go

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
package session
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"fmt"
8+
"reflect"
9+
"time"
10+
11+
"github.com/davecgh/go-spew/spew"
12+
"github.com/lightninglabs/lightning-terminal/accounts"
13+
"github.com/lightninglabs/lightning-terminal/db/sqlc"
14+
"github.com/pmezard/go-difflib/difflib"
15+
)
16+
17+
var (
18+
// ErrMigrationMismatch is returned when the migrated session does not
19+
// match the original session.
20+
ErrMigrationMismatch = fmt.Errorf("migrated session does not match " +
21+
"original session")
22+
)
23+
24+
// MigrateSessionStoreToSQL runs the migration of all sessions from the KV
25+
// database to the SQL database. The migration is done in a single transaction
26+
// to ensure that all sessions are migrated or none at all.
27+
//
28+
// NOTE: As sessions may contain linked accounts, the accounts sql migration
29+
// MUST be run prior to this migration.
30+
func MigrateSessionStoreToSQL(ctx context.Context, kvStore *BoltStore,
31+
tx SQLQueries) error {
32+
33+
log.Infof("Starting migration of the KV sessions store to SQL")
34+
35+
kvSessions, err := kvStore.ListAllSessions(ctx)
36+
if err != nil {
37+
return err
38+
}
39+
40+
// If sessions are linked to a group, we must insert the initial session
41+
// of each group before the other sessions in that group. This ensures
42+
// we can retrieve the SQL group ID when inserting the remaining
43+
// sessions. Therefore, we first insert all initial group sessions,
44+
// allowing us to fetch the group IDs and insert the rest of the
45+
// sessions afterward.
46+
// We therefore filter out the initial sessions first, and then migrate
47+
// them prior to the rest of the sessions.
48+
var (
49+
initialGroupSessions []*Session
50+
linkedSessions []*Session
51+
)
52+
53+
for _, kvSession := range kvSessions {
54+
if kvSession.GroupID == kvSession.ID {
55+
initialGroupSessions = append(
56+
initialGroupSessions, kvSession,
57+
)
58+
} else {
59+
linkedSessions = append(linkedSessions, kvSession)
60+
}
61+
}
62+
63+
err = migrateSessionsToSQLAndValidate(ctx, tx, initialGroupSessions)
64+
if err != nil {
65+
return fmt.Errorf("migration of non-linked session failed: %w",
66+
err)
67+
}
68+
69+
err = migrateSessionsToSQLAndValidate(ctx, tx, linkedSessions)
70+
if err != nil {
71+
return fmt.Errorf("migration of linked session failed: %w", err)
72+
}
73+
74+
total := len(initialGroupSessions) + len(linkedSessions)
75+
log.Infof("All sessions migrated from KV to SQL. Total number of "+
76+
"sessions migrated: %d", total)
77+
78+
return nil
79+
}
80+
81+
// migrateSessionsToSQLAndValidate runs the migration for the passed sessions
82+
// from the KV database to the SQL database, and validates that the migrated
83+
// sessions match the original sessions.
84+
func migrateSessionsToSQLAndValidate(ctx context.Context,
85+
tx SQLQueries, kvSessions []*Session) error {
86+
87+
for _, kvSession := range kvSessions {
88+
err := migrateSingleSessionToSQL(ctx, tx, kvSession)
89+
if err != nil {
90+
return fmt.Errorf("unable to migrate session(%v): %w",
91+
kvSession.ID, err)
92+
}
93+
94+
// Validate that the session was correctly migrated and matches
95+
// the original session in the kv store.
96+
sqlSess, err := tx.GetSessionByAlias(ctx, kvSession.ID[:])
97+
if err != nil {
98+
if errors.Is(err, sql.ErrNoRows) {
99+
err = ErrSessionNotFound
100+
}
101+
return fmt.Errorf("unable to get migrated session "+
102+
"from sql store: %w", err)
103+
}
104+
105+
migratedSession, err := unmarshalSession(ctx, tx, sqlSess)
106+
if err != nil {
107+
return fmt.Errorf("unable to unmarshal migrated "+
108+
"session: %w", err)
109+
}
110+
111+
overrideSessionTimeZone(kvSession)
112+
overrideSessionTimeZone(migratedSession)
113+
overrideMacaroonRecipe(kvSession, migratedSession)
114+
115+
if !reflect.DeepEqual(kvSession, migratedSession) {
116+
diff := difflib.UnifiedDiff{
117+
A: difflib.SplitLines(
118+
spew.Sdump(kvSession),
119+
),
120+
B: difflib.SplitLines(
121+
spew.Sdump(migratedSession),
122+
),
123+
FromFile: "Expected",
124+
FromDate: "",
125+
ToFile: "Actual",
126+
ToDate: "",
127+
Context: 3,
128+
}
129+
diffText, _ := difflib.GetUnifiedDiffString(diff)
130+
131+
return fmt.Errorf("%w: %v.\n%v", ErrMigrationMismatch,
132+
kvSession.ID, diffText)
133+
}
134+
}
135+
136+
return nil
137+
}
138+
139+
// migrateSingleSessionToSQL runs the migration for a single session from the
140+
// KV database to the SQL database. Note that if the session links to an
141+
// account, the linked accounts store MUST have been migrated before that
142+
// session is migrated.
143+
func migrateSingleSessionToSQL(ctx context.Context,
144+
tx SQLQueries, session *Session) error {
145+
146+
var (
147+
acctID sql.NullInt64
148+
err error
149+
remotePubKey []byte
150+
)
151+
152+
session.AccountID.WhenSome(func(alias accounts.AccountID) {
153+
// Check that the account exists in the SQL store, before
154+
// linking it.
155+
var acctAlias int64
156+
acctAlias, err = alias.ToInt64()
157+
if err != nil {
158+
return
159+
}
160+
161+
var acctDBID int64
162+
acctDBID, err = tx.GetAccountIDByAlias(ctx, acctAlias)
163+
if errors.Is(err, sql.ErrNoRows) {
164+
err = accounts.ErrAccNotFound
165+
return
166+
} else if err != nil {
167+
return
168+
}
169+
170+
acctID = sql.NullInt64{
171+
Int64: acctDBID,
172+
Valid: true,
173+
}
174+
})
175+
if err != nil {
176+
return err
177+
}
178+
179+
// The remote public key is currently only set for autopilot sessions,
180+
// else it's a nil byte array.
181+
if session.RemotePublicKey != nil {
182+
remotePubKey = session.RemotePublicKey.SerializeCompressed()
183+
}
184+
185+
// Proceed to insert the session into the sql db.
186+
insertSessionParams := sqlc.InsertSessionParams{
187+
Alias: session.ID[:],
188+
Label: session.Label,
189+
State: int16(session.State),
190+
Type: int16(session.Type),
191+
Expiry: session.Expiry.UTC(),
192+
CreatedAt: session.CreatedAt.UTC(),
193+
ServerAddress: session.ServerAddr,
194+
DevServer: session.DevServer,
195+
MacaroonRootKey: int64(session.MacaroonRootKey),
196+
PairingSecret: session.PairingSecret[:],
197+
LocalPrivateKey: session.LocalPrivateKey.Serialize(),
198+
LocalPublicKey: session.LocalPublicKey.SerializeCompressed(),
199+
RemotePublicKey: remotePubKey,
200+
Privacy: session.WithPrivacyMapper,
201+
AccountID: acctID,
202+
}
203+
204+
sqlId, err := tx.InsertSession(ctx, insertSessionParams)
205+
if err != nil {
206+
return err
207+
}
208+
209+
// Since the InsertSession query doesn't support that we set the revoked
210+
// field during the insert, we need to set the field after the session
211+
// has been created.
212+
if !session.RevokedAt.IsZero() {
213+
setSessionRevokedParams := sqlc.SetSessionRevokedAtParams{
214+
ID: sqlId,
215+
RevokedAt: sql.NullTime{
216+
Time: session.RevokedAt.UTC(),
217+
Valid: true,
218+
},
219+
}
220+
221+
err = tx.SetSessionRevokedAt(ctx, setSessionRevokedParams)
222+
if err != nil {
223+
return err
224+
}
225+
}
226+
227+
// After the session has been inserted, we need to update the session
228+
// with the group ID if it is linked to a group. We need to do this
229+
// after the session has been inserted, because the group ID can be the
230+
// session itself, and therefore the SQL id for the session won't exist
231+
// prior to inserting the session.
232+
groupID, err := tx.GetSessionIDByAlias(ctx, session.GroupID[:])
233+
if errors.Is(err, sql.ErrNoRows) {
234+
return ErrUnknownGroup
235+
} else if err != nil {
236+
return fmt.Errorf("unable to fetch group(%x): %w",
237+
session.GroupID[:], err)
238+
}
239+
240+
// Now lets set the group ID for the session.
241+
err = tx.SetSessionGroupID(ctx, sqlc.SetSessionGroupIDParams{
242+
ID: sqlId,
243+
GroupID: sql.NullInt64{
244+
Int64: groupID,
245+
Valid: true,
246+
},
247+
})
248+
if err != nil {
249+
return fmt.Errorf("unable to set group Alias: %w", err)
250+
}
251+
252+
// Once we have the sqlID for the session, we can proceed to insert rows
253+
// into the linked child tables.
254+
if session.MacaroonRecipe != nil {
255+
// We start by inserting the macaroon permissions.
256+
for _, sessionPerm := range session.MacaroonRecipe.Permissions {
257+
permParam := sqlc.InsertSessionMacaroonPermissionParams{
258+
SessionID: sqlId,
259+
Entity: sessionPerm.Entity,
260+
Action: sessionPerm.Action,
261+
}
262+
263+
err = tx.InsertSessionMacaroonPermission(
264+
ctx, permParam,
265+
)
266+
if err != nil {
267+
return err
268+
}
269+
}
270+
271+
// Next we insert the macaroon caveats.
272+
for _, sessCaveat := range session.MacaroonRecipe.Caveats {
273+
caveatParams := sqlc.InsertSessionMacaroonCaveatParams{
274+
SessionID: sqlId,
275+
CaveatID: sessCaveat.Id,
276+
VerificationID: sessCaveat.VerificationId,
277+
Location: sql.NullString{
278+
String: sessCaveat.Location,
279+
Valid: sessCaveat.Location != "",
280+
},
281+
}
282+
283+
err = tx.InsertSessionMacaroonCaveat(
284+
ctx, caveatParams,
285+
)
286+
if err != nil {
287+
return err
288+
}
289+
}
290+
}
291+
292+
// That's followed by the feature config.
293+
if session.FeatureConfig != nil {
294+
for featureName, config := range *session.FeatureConfig {
295+
fConfParams := sqlc.InsertSessionFeatureConfigParams{
296+
SessionID: sqlId,
297+
FeatureName: featureName,
298+
Config: config,
299+
}
300+
301+
err = tx.InsertSessionFeatureConfig(ctx, fConfParams)
302+
if err != nil {
303+
return err
304+
}
305+
}
306+
}
307+
308+
// Finally we insert the privacy flags.
309+
for _, privacyFlag := range session.PrivacyFlags {
310+
privacyFlagParams := sqlc.InsertSessionPrivacyFlagParams{
311+
SessionID: sqlId,
312+
Flag: int32(privacyFlag),
313+
}
314+
315+
err = tx.InsertSessionPrivacyFlag(
316+
ctx, privacyFlagParams,
317+
)
318+
if err != nil {
319+
return err
320+
}
321+
}
322+
323+
return nil
324+
}
325+
326+
// overrideSessionTimeZone overrides the time zone of the session to the local
327+
// time zone and chops off the nanosecond part for comparison. This is needed
328+
// because KV database stores times as-is which as an unwanted side effect would
329+
// fail migration due to time comparison expecting both the original and
330+
// migrated sessions to be in the same local time zone and in microsecond
331+
// precision. Note that PostgresSQL stores times in microsecond precision while
332+
// SQLite can store times in nanosecond precision if using TEXT storage class.
333+
func overrideSessionTimeZone(session *Session) {
334+
fixTime := func(t time.Time) time.Time {
335+
return t.In(time.Local).Truncate(time.Microsecond)
336+
}
337+
338+
if !session.Expiry.IsZero() {
339+
session.Expiry = fixTime(session.Expiry)
340+
}
341+
342+
if !session.CreatedAt.IsZero() {
343+
session.CreatedAt = fixTime(session.CreatedAt)
344+
}
345+
346+
if !session.RevokedAt.IsZero() {
347+
session.RevokedAt = fixTime(session.RevokedAt)
348+
}
349+
}
350+
351+
// overrideMacaroonRecipe overrides the MacaroonRecipe for the SQL session in a
352+
// certain scenario:
353+
// In the bbolt store, a session can have a non-nil macaroon struct, despite
354+
// both the permissions and caveats being nil. There is no way to represent this
355+
// in the SQL store, as the macaroon permissions and caveats are separate
356+
// tables. Therefore, in the scenario where a MacaroonRecipe exists for the
357+
// bbolt version, but both the permissions and caveats are nil, we override the
358+
// MacaroonRecipe for the SQL version and set it to a MacaroonRecipe with
359+
// nil permissions and caveats. This is needed to ensure that the deep equals
360+
// check in the migration validation does not fail in this scenario.
361+
func overrideMacaroonRecipe(kvSession *Session, migratedSession *Session) {
362+
if kvSession.MacaroonRecipe != nil {
363+
kvPerms := kvSession.MacaroonRecipe.Permissions
364+
kvCaveats := kvSession.MacaroonRecipe.Caveats
365+
366+
if kvPerms == nil && kvCaveats == nil {
367+
migratedSession.MacaroonRecipe = &MacaroonRecipe{}
368+
}
369+
}
370+
}

0 commit comments

Comments
 (0)