Skip to content

Commit bce4c04

Browse files
authored
Merge pull request #1133 from ViktorTigerstrom/2025-08-session-migration-duplicate-id-fix
[sql-50] session migration duplicate ID fix
2 parents 64f56dc + dfe0fa8 commit bce4c04

File tree

2 files changed

+273
-46
lines changed

2 files changed

+273
-46
lines changed

session/sql_migration.go

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)