@@ -41,35 +41,16 @@ func MigrateSessionStoreToSQL(ctx context.Context, kvStore *bbolt.DB,
4141 return err
4242 }
4343
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 )
6645
46+ // Migrate the non-linked sessions first.
6747 err = migrateSessionsToSQLAndValidate (ctx , tx , initialGroupSessions )
6848 if err != nil {
6949 return fmt .Errorf ("migration of non-linked session failed: %w" ,
7050 err )
7151 }
7252
53+ // Then migrate the linked sessions.
7354 err = migrateSessionsToSQLAndValidate (ctx , tx , linkedSessions )
7455 if err != nil {
7556 return fmt .Errorf ("migration of linked session failed: %w" , err )
@@ -82,6 +63,73 @@ func MigrateSessionStoreToSQL(ctx context.Context, kvStore *bbolt.DB,
8263 return nil
8364}
8465
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+
85133// getBBoltSessions is a helper function that fetches all sessions from the
86134// Bbolt store, by iterating directly over the buckets, without needing to
87135// use any public functions of the BoltStore struct.
0 commit comments