@@ -41,35 +41,16 @@ func MigrateSessionStoreToSQL(ctx context.Context, kvStore *bbolt.DB,
41
41
return err
42
42
}
43
43
44
- // If sessions are linked to a group, we must insert the initial session
45
- // of each group before the other sessions in that group. This ensures
46
- // we can retrieve the SQL group ID when inserting the remaining
47
- // sessions. Therefore, we first insert all initial group sessions,
48
- // allowing us to fetch the group IDs and insert the rest of the
49
- // sessions afterward.
50
- // We therefore filter out the initial sessions first, and then migrate
51
- // them prior to the rest of the sessions.
52
- var (
53
- initialGroupSessions []* Session
54
- linkedSessions []* Session
55
- )
56
-
57
- for _ , kvSession := range kvSessions {
58
- if kvSession .GroupID == kvSession .ID {
59
- initialGroupSessions = append (
60
- initialGroupSessions , kvSession ,
61
- )
62
- } else {
63
- linkedSessions = append (linkedSessions , kvSession )
64
- }
65
- }
44
+ initialGroupSessions , linkedSessions := filterSessions (kvSessions )
66
45
46
+ // Migrate the non-linked sessions first.
67
47
err = migrateSessionsToSQLAndValidate (ctx , tx , initialGroupSessions )
68
48
if err != nil {
69
49
return fmt .Errorf ("migration of non-linked session failed: %w" ,
70
50
err )
71
51
}
72
52
53
+ // Then migrate the linked sessions.
73
54
err = migrateSessionsToSQLAndValidate (ctx , tx , linkedSessions )
74
55
if err != nil {
75
56
return fmt .Errorf ("migration of linked session failed: %w" , err )
@@ -82,6 +63,73 @@ func MigrateSessionStoreToSQL(ctx context.Context, kvStore *bbolt.DB,
82
63
return nil
83
64
}
84
65
66
+ // filterSessions categorizes the sessions into two groups: initial group
67
+ // sessions and linked sessions. The initial group sessions are the first
68
+ // sessions in a session group, while the linked sessions are those that have a
69
+ // linked parent session. These are separated to ensure that we can insert the
70
+ // initial group sessions first, which allows us to fetch the SQL group ID when
71
+ // inserting the rest of the linked sessions afterward.
72
+ //
73
+ // Additionally, it checks for duplicate session IDs and drops all but
74
+ // one session with the same ID, keeping the one with the latest CreatedAt
75
+ // timestamp. Note that users with duplicate session IDs should be extremely
76
+ // rare, as it could only occur if colliding session IDs were created prior to
77
+ // the introduction of the session linking functionality.
78
+ func filterSessions (kvSessions []* Session ) ([]* Session , []* Session ) {
79
+ // First map sessions by their ID.
80
+ sessionsByID := make (map [ID ][]* Session )
81
+ for _ , s := range kvSessions {
82
+ sessionsByID [s .ID ] = append (sessionsByID [s .ID ], s )
83
+ }
84
+
85
+ var (
86
+ initialGroupSessions []* Session
87
+ linkedSessions []* Session
88
+ )
89
+
90
+ // Process the mapped sessions. If there are duplicate sessions with the
91
+ // same ID, we will only iterate the session with the latest CreatedAt
92
+ // timestamp, and drop the other sessions. This is to ensure that we can
93
+ // keep a UNIQUE constraint for the session ID (alias) in the SQL db.
94
+ for id , sessions := range sessionsByID {
95
+ sessionToKeep := sessions [0 ]
96
+ if len (sessions ) > 1 {
97
+ log .Warnf ("Found %d sessions with duplicate ID %x, " +
98
+ "keeping only the latest one" , len (sessions ),
99
+ id )
100
+
101
+ // Find the session with the latest timestamp.
102
+ latestSession := sessions [0 ]
103
+ for _ , s := range sessions [1 :] {
104
+ if s .CreatedAt .After (latestSession .CreatedAt ) {
105
+ latestSession = s
106
+ }
107
+ }
108
+ sessionToKeep = latestSession
109
+
110
+ // Log the sessions that will be dropped.
111
+ for _ , s := range sessions {
112
+ if s == sessionToKeep {
113
+ continue
114
+ }
115
+ log .Warnf ("Dropping duplicate session with ID " +
116
+ "%x created at %v" , id , s .CreatedAt )
117
+ }
118
+ }
119
+
120
+ // Categorize the session that we are keeping.
121
+ if sessionToKeep .GroupID == sessionToKeep .ID {
122
+ initialGroupSessions = append (
123
+ initialGroupSessions , sessionToKeep ,
124
+ )
125
+ } else {
126
+ linkedSessions = append (linkedSessions , sessionToKeep )
127
+ }
128
+ }
129
+
130
+ return initialGroupSessions , linkedSessions
131
+ }
132
+
85
133
// getBBoltSessions is a helper function that fetches all sessions from the
86
134
// Bbolt store, by iterating directly over the buckets, without needing to
87
135
// use any public functions of the BoltStore struct.
0 commit comments