Skip to content

fix: replace ReadWriteLock with NSLock for consistent thread-safety - #476

Merged
ioannisj merged 18 commits into
PostHog:mainfrom
Kilo-Loco:fix/file-backed-queue-thread-safety
Mar 18, 2026
Merged

fix: replace ReadWriteLock with NSLock for consistent thread-safety#476
ioannisj merged 18 commits into
PostHog:mainfrom
Kilo-Loco:fix/file-backed-queue-thread-safety

Conversation

@Kilo-Loco

@Kilo-Loco Kilo-Loco commented Feb 17, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

The @ReadWriteLock property wrapper provides a false sense of thread-safety for collection types because the lock is acquired and released between separate operations, allowing the underlying collection to change between a check and a mutation.

💚 How did you test it?

Added concurrency tests covering concurrent adds, deletes, and mixed operations.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

@Kilo-Loco
Kilo-Loco requested a review from a team as a code owner February 17, 2026 00:32
@Kilo-Loco
Kilo-Loco force-pushed the fix/file-backed-queue-thread-safety branch from 043ca0b to d6a6c47 Compare February 17, 2026 00:38
@Kilo-Loco
Kilo-Loco force-pushed the fix/file-backed-queue-thread-safety branch from d6a6c47 to 88e6244 Compare February 17, 2026 00:38
do {
let filename = "\(Date().timeIntervalSince1970)"
try contents.write(to: queue.appendingPathComponent(filename))
items.append(filename)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pop method is only called when PostHogQueue acquires isFlushingLock
PostHogQueue calls PostHogFileBackedQueue add and delete in the same method
we also call peek/clear from PostHogQueue
maybe a single lock for all those operations within the PostHogQueue would be better?
i'll let @ioannisj figure this out
but i do see some possible race conditions, good catch @Kilo-Loco

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed additional concurrency tests that expose a race condition in the queue. when multiple threads call add() simultaneously, they can generate identical filenames since Date().timeIntervalSince1970 and file operations are not atomic.

To clarify, this was always there and not added with this PR, but since we are working on this we should address it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So one operation can overwrite the file of another add operation which can lead to data loss, if my rationale is correct here

@marandaneto
marandaneto requested a review from ioannisj February 17, 2026 12:21
@ioannisj

Copy link
Copy Markdown
Contributor

Hey @Kilo-Loco, thank you for this. I think what we need to do is remove the dependance on @ReadWriteLock property wrapper since it's indeed problematic and provides a false sense of protection against collection types specifically. So a simple NSLock around items here could potentially be safer and communicate intent better

Having said that, I can only see this as problematic since the rest of the operations should be protected by @ReadWriteLock already:

if items.isEmpty { return }      // lock acquired, check, lock released
let removed = items.remove(at: index)  // lock acquired again, but items may have changed!

@Kilo-Loco

Kilo-Loco commented Feb 19, 2026

Copy link
Copy Markdown
Contributor Author

Got it. I can remove the dependencies on @ReadWriteLock here, but now I'm wondering if the property wrapper should be removed altogether 🤔 I'm only seeing it be used here and in PostHogContext so maybe we can change both places for consistency across the codebase. I can scope the work in an issue to properly document the changes or just make the changes and update the PR body. Let me know the preferred path forward for the team.

@ioannisj

Copy link
Copy Markdown
Contributor

Got it. I can remove the dependencies on @ReadWriteLock here, but now I'm wondering if the property wrapper should be removed altogether 🤔 I'm only seeing it be used here and in PostHogContext so maybe we can change both places for consistency across the codebase. I can scope the work in an issue to properly document the changes or just make the changes and update the PR body. Let me know the preferred path forward for the team.

Hey @Kilo-Loco I think we can remove ReadWriteLock all-together, agreed. It's better we keep locking consistent across the code and this is the only one that's sticking out. I'd say this can be done in this PR, just update title and body. We just migrated to changeset based release process so make sure to merge main

- Remove @ReadWriteLock property wrapper in favor of explicit NSLock
- Update PostHogFileBackedQueue to use NSLock for items array
- Update PostHogContext to use NSLock for screenSize
- Delete ReadWriteLock.swift (no longer used)

This provides clearer intent and consistency with the rest of the codebase.
@Kilo-Loco Kilo-Loco changed the title fix: use atomic mutate() for thread-safe array operations in FileBackedQueue fix: replace ReadWriteLock with NSLock for consistent thread-safety Feb 21, 2026
@Kilo-Loco
Kilo-Loco force-pushed the fix/file-backed-queue-thread-safety branch from b838ce8 to 10d65c2 Compare February 22, 2026 20:31
@Kilo-Loco

Copy link
Copy Markdown
Contributor Author
  • Removed @ReadWriteLock property wrapper dependency
  • Replaced with explicit NSLock for clearer intent
  • Merged main
  • Tests passing

Let me know if anything needs adjusting.

@ioannisj ioannisj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LG, left a nit comment
Let's add a changeset and label this PR and we are good to go I think

Comment thread PostHog/PostHogContext.swift Outdated
Comment on lines +271 to +273
screenSizeLock.lock()
let currentScreenSize = screenSize
screenSizeLock.unlock()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: prefer .withLock which has a generic return type. Commenting just here but it can be replaced in multiple places

Suggested change
screenSizeLock.lock()
let currentScreenSize = screenSize
screenSizeLock.unlock()
let currentScreenSize = screenSizeLock.withLock { screenSize }

Comment thread CHANGELOG.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We switched to changeset based releases. So merge main and pnpm changeset + label this PR with release instead (details in RELEASING.md)

- Replace manual lock()/unlock() with .withLock closures
- Add changeset for release
@Kilo-Loco

Copy link
Copy Markdown
Contributor Author
  • Updated call sites to use .withLock block
  • Add changeset file

Ready to be labeled with release if everything else looks good 👍🏽

@ioannisj

Copy link
Copy Markdown
Contributor

@marandaneto I'll need your eyes on 2a443ab please. Addressing this comment here. This should be backward compatible with old filenames so no migration needed

Comment thread PostHog/PostHogFileBackedQueue.swift Outdated
func add(_ contents: Data) {
do {
let filename = "\(Date().timeIntervalSince1970)"
let filename = "\(Date().timeIntervalSince1970)-\(UUID.v7().uuidString)"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use only (UUID.v7().uuidString instead?
uuid v7 already has a timestamp in its string
you can also list files from a directory and sort by timestamp from its metadata (we do that on android btw)

@ioannisj ioannisj Mar 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea here was to be compatible when mixed with old file names. v7 is sortable which is very helpful, but I'll have to think how to handle the mix of UUIDv7 and old timestamps. I'll think about it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can also list files from a directory and sort by timestamp from its metadata (we do that on android btw)

yeah probably this is the way

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i just mentioned above how we do this for android
this should be compatible with whatever name the files are
see example https://github.com/PostHog/posthog-android/blob/65700f9159a789861b35dac1f936f76bb0683807/posthog/src/main/java/com/posthog/internal/PostHogQueue.kt#L417-L422

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeIntervalSince1970-v7 is also fine, not a blocker, just not needed because the approach above would solve the sorting

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay implemented by modification date c33a357 even though API is not as elegant as Android

@marandaneto

Copy link
Copy Markdown
Member

@marandaneto I'll need your eyes on 2a443ab please. Addressing this comment here. This should be backward compatible with old filenames so no migration needed

replied

@ioannisj ioannisj added release and removed release labels Mar 18, 2026
@ioannisj
ioannisj merged commit 061cb44 into PostHog:main Mar 18, 2026
28 of 30 checks passed
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.

3 participants