Skip to content

fix(gsession): atomic write in SetSession to prevent concurrent read race (#4792) - #4814

Open
waterWang wants to merge 2 commits into
gogf:masterfrom
waterWang:fix/atomic-session-write
Open

fix(gsession): atomic write in SetSession to prevent concurrent read race (#4792)#4814
waterWang wants to merge 2 commits into
gogf:masterfrom
waterWang:fix/atomic-session-write

Conversation

@waterWang

Copy link
Copy Markdown
Contributor

Fix: Atomic write in SetSession to prevent concurrent read race

Description

StorageFile.SetSession uses a non-atomic file write pattern (open with O_TRUNC then two separate Write calls), creating a race window where concurrent readers see an empty or partially-written file.

Root Cause

The write path:

  1. Opens file with O_TRUNC — immediately empties it
  2. file.Write(timestamp) — writes 8 bytes
  3. file.Write(content) — writes JSON body

Between steps 1 and 3, any concurrent reader calling GetSession sees a file with ≤8 bytes and returns nil — the user appears "not logged in".

Fix

Replace the O_TRUNC + two Write() calls with atomic write via temp file + os.Rename():

  1. Build the complete content (8-byte timestamp + JSON body) in memory
  2. Write to a .tmp file
  3. os.Rename() the temp file to the target path

os.Rename() is atomic on the same filesystem (POSIX guarantee), so readers always see a complete file.

Testing

  • The existing tests in os/gsession/gsession_z_unit_storage_file_test.go cover the session lifecycle
  • The fix is a drop-in replacement — same data format, same file path, no new dependencies
  • os.Rename is used elsewhere in the Go ecosystem for atomic file writes (e.g., os.WriteFile itself is atomic on most systems, but we use temp+rename for explicit POSIX atomicity)

Fixes #4792

…f#4787)

When Result.Structs / Model.Scan is called with a *[]map[string]interface{} pointer,
the destination element type is reflect.Map, but converter.Structs routes it through
c.Struct which expects a struct type. This causes GetCachedStructInfo to return nil
for non-struct types, silently producing empty maps.

Fix: add a reflect.Map branch that calls c.MapToMap for each element instead of c.Struct.
This matches the behavior of gconv.Scan which already handles map-slice conversion via
MapToMaps.
return err
// Build content with 8-byte timestamp prefix (same format as before).
timestamp := gbinary.EncodeInt64(gtime.TimestampMilli())
data := make([]byte, 8+len(content))
// Atomic write: write to temp file first, then rename.
// This prevents concurrent readers from seeing an empty/partial file.
tmpPath := path + ".tmp"
if err := os.WriteFile(tmpPath, data, os.ModePerm); err != nil {
if _, err = file.Write(content); err != nil {
err = gerror.Wrapf(err, `write data failed to file "%s"`, path)
return err
if err := os.Rename(tmpPath, path); err != nil {
if _, err = file.Write(content); err != nil {
err = gerror.Wrapf(err, `write data failed to file "%s"`, path)
return err
if err := os.Rename(tmpPath, path); err != nil {
err = gerror.Wrapf(err, `write data failed to file "%s"`, path)
return err
if err := os.Rename(tmpPath, path); err != nil {
os.Remove(tmpPath)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[gsession] StorageFile.SetSession 非原子写入导致并发读请求看到空 session

2 participants