Skip to content

[bug]: added lock to remove race conditions.#20

Open
Thedrogon wants to merge 1 commit intoanikchand461:mainfrom
Thedrogon:fix/racing_conditions
Open

[bug]: added lock to remove race conditions.#20
Thedrogon wants to merge 1 commit intoanikchand461:mainfrom
Thedrogon:fix/racing_conditions

Conversation

@Thedrogon
Copy link
Contributor

@Thedrogon Thedrogon commented Feb 16, 2026

closes #19 ,

This PR introduces a file locking mechanism to ensure data integrity when multiple instances of the task CLI are run concurrently . It also fixes a .gitignore issue that was incorrectly masking source files.

Added lock.go: Implements a cross-platform lock file mechanism (tasks.lock) alongside the data file. It attempts to acquire a lock for up to 1 second before timing out.

Updated main.go: Wraps the command execution in AcquireLock() and ensures the lock is released via defer.
Updated .gitignore: Changed task to /task to ensure the binary is ignored without hiding the task/ source directory or new files like lock.go.

Summary by Sourcery

Add a file-based locking mechanism to prevent concurrent task CLI instances from corrupting data and adjust ignore rules for the compiled binary.

Bug Fixes:

  • Prevent race conditions and data corruption by serializing task operations with a lock file.

Enhancements:

  • Introduce a reusable lock acquisition helper in the task package that manages lock lifecycle around command execution.

Build:

  • Update .gitignore to ignore only the compiled task binary without hiding the source directory.

Summary by CodeRabbit

  • Bug Fixes
    • Added file locking mechanism to prevent conflicts when accessing tasks concurrently.

@vercel
Copy link

vercel bot commented Feb 16, 2026

@Thedrogon is attempting to deploy a commit to the Anik Chand's projects Team on Vercel.

A member of the Team first needs to authorize it.

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 16, 2026

Reviewer's Guide

Introduces a file-based locking mechanism around task CLI operations to avoid concurrent access race conditions, and adjusts .gitignore so the built binary is ignored without hiding source files.

Sequence diagram for task CLI execution with file lock

sequenceDiagram
    actor User
    participant Shell
    participant TaskCLI as cmd_task_main
    participant TaskPkg as internal_task
    participant OS

    User->>Shell: Run task command
    Shell->>TaskCLI: Execute main
    TaskCLI->>TaskPkg: AcquireLock()
    TaskPkg->>TaskPkg: Compute dataFilePath
    TaskPkg->>OS: Try create tasks.lock (O_CREATE | O_EXCL) up to 10 times
    alt Lock acquired
        OS-->>TaskPkg: File created
        TaskPkg-->>TaskCLI: unlockFunc, nil
        TaskCLI->>TaskCLI: defer unlockFunc()
        TaskCLI->>TaskCLI: Parse os.Args[1]
        TaskCLI->>TaskPkg: Execute selected command
        TaskCLI->>TaskPkg: unlockFunc()
        TaskPkg->>OS: Remove tasks.lock
    else Lock not acquired
        OS-->>TaskPkg: tasks.lock exists on all attempts
        TaskPkg-->>TaskCLI: nil, error
        TaskCLI-->>Shell: Print error and exit
    end
Loading

Class diagram for main and new lock function in task package

classDiagram
    class cmd_task_main {
        +main()
    }

    class internal_task {
        +AcquireLock() (func(), error)
        +dataFilePath() (string, error)
    }

    cmd_task_main ..> internal_task : calls AcquireLock
    internal_task ..> internal_task : calls dataFilePath
Loading

Flow diagram for AcquireLock retry and error handling

flowchart TD
    A[Start AcquireLock] --> B[Call dataFilePath]
    B -->|error| Z[Return nil and error]
    B -->|ok| C[Compute lockPath tasks.lock]
    C --> D[Set attempt i = 0]
    D --> E{Attempt i < 10?}
    E -->|no| Y[Return nil and could not acquire lock error]
    E -->|yes| F[os.OpenFile lockPath with O_CREATE and O_EXCL]
    F --> G{OpenFile error is nil?}
    G -->|yes| H[Close lock file]
    H --> I[Return unlock func that removes lockPath]
    G -->|no| J{os.IsExist error?}
    J -->|no| K[Return nil and error]
    J -->|yes| L[Sleep 100ms]
    L --> M[Increment i]
    M --> E
Loading

File-Level Changes

Change Details Files
Add a cross-process file lock around task database access to prevent concurrent CLI instances from corrupting data.
  • Introduce AcquireLock that derives a lock file path from the existing data file location and uses an exclusive create to signal ownership.
  • Implement retry logic that attempts to create the lock file up to 10 times with 100ms sleeps (1 second total) before failing with a descriptive error.
  • Return an unlock callback that removes the lock file, ensuring cleanup when the caller defers it.
internal/task/lock.go
Wrap CLI command dispatch with the new lock acquisition to serialize concurrent executions.
  • Call task.AcquireLock() early in main, after validating arguments but before the command switch executes.
  • Handle lock acquisition failures by printing the error and exiting without executing any commands.
  • Use defer on the returned unlock function to guarantee the lock file is removed on normal exit paths.
cmd/task/main.go
Fix .gitignore so the compiled task binary is ignored without masking the task source directory or new Go files.
  • Change the ignore pattern from a bare task to /task so only the root-level binary is ignored.
  • Ensure that the internal task package directory and new files like lock.go are no longer unintentionally ignored.
.gitignore

Assessment against linked issues

Issue Objective Addressed Explanation
#19 Implement a file locking mechanism so that only one instance of the task CLI can read/modify/write the task database file at a time, preventing data loss from concurrent commands (e.g., simultaneous task done and task add).

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

The pull request implements a file locking mechanism to serialize access to the task database. A lock file is acquired in the main routine before command processing, with automatic retry logic and deterministic cleanup to prevent concurrent data races.

Changes

Cohort / File(s) Summary
Lock Implementation
internal/task/lock.go, cmd/task/main.go
Introduces AcquireLock() function with atomic file creation, 10-attempt retry logic (100ms intervals), and a deferred cleanup function to release the lock. Main routine now acquires lock before command dispatch with early-exit on failure.
Project Configuration
.gitignore
Changed ignore pattern from global "task" to root-only "/task".

Sequence Diagram

sequenceDiagram
    participant Main as Main Routine
    participant Lock as Lock Manager
    participant FS as File System
    participant Cmd as Command Handler

    Main->>Lock: AcquireLock()
    Lock->>FS: Attempt atomic lock file creation (O_CREATE|O_EXCL)
    alt Lock acquired
        FS-->>Lock: Lock file created
        Lock-->>Main: Return cleanup function
        Main->>Cmd: Process commands
        Cmd->>Cmd: Read/modify/write task data
        Cmd-->>Main: Commands complete
        Main->>Lock: defer cleanup()
        Lock->>FS: Remove lock file
        FS-->>Lock: Success
    else Lock exists (retry)
        FS-->>Lock: File exists error
        Lock->>Lock: Wait 100ms
        Lock->>FS: Retry (up to 10 times)
    else Lock acquisition failed
        Lock-->>Main: Return error
        Main->>Main: Exit early
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A lock, a hop, and files so neat,
One task at a time, sequential beat!
No more races, no data loss,
Atomic guards across the boss!
With every retry, patience we earn,
As safely our little tasks all turn! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (3 files):

⚔️ .gitignore (content)
⚔️ cmd/task/main.go (content)
⚔️ internal/task/add.go (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a lock mechanism to address race conditions in file operations.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #19: a lockfile mechanism that serializes access to the data file, preventing concurrent read-modify-write races.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #19. The .gitignore fix is necessary to prevent the compiled binary from masking the task/ source directory.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix/racing_conditions
  • Post resolved changes as copyable diffs in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The lock file is treated as a simple presence flag; consider writing minimal metadata (e.g., PID and timestamp) and handling stale locks so a crashed process doesn’t leave the CLI permanently blocked on future runs.
  • The unlock function currently ignores any error from os.Remove(lockPath); if a remove failure would matter for subsequent runs, consider checking/logging that error so unexpected filesystem issues with the lock don’t go unnoticed.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The lock file is treated as a simple presence flag; consider writing minimal metadata (e.g., PID and timestamp) and handling stale locks so a crashed process doesn’t leave the CLI permanently blocked on future runs.
- The unlock function currently ignores any error from `os.Remove(lockPath)`; if a remove failure would matter for subsequent runs, consider checking/logging that error so unexpected filesystem issues with the lock don’t go unnoticed.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
internal/task/lock.go (3)

22-25: Lock file is created then immediately closed — OS-level lock is never held.

The file is opened with O_CREATE|O_EXCL, then immediately closed on Line 25. Between f.Close() and the eventual os.Remove(lockPath), the lock is purely advisory based on file existence. This is the root cause of the stale-lock vulnerability described above.

If you switch to syscall.Flock (or golang.org/x/sys/windows equivalent), you'd keep the file descriptor open and the OS would release the lock on process exit:

♻️ Sketch using syscall.Flock (Unix)
-	f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL, 0644)
+	f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644)
 	if err == nil {
-		f.Close()
-		return func() {
-			os.Remove(lockPath)
-		}, nil
+		if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err == nil {
+			return func() {
+				syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
+				f.Close()
+				os.Remove(lockPath)
+			}, nil
+		}
+		f.Close()
 	}

20-39: 1-second timeout may be too aggressive for legitimate contention.

Ten retries × 100 ms = 1 second. For a CLI that might be invoked by a script in a tight loop (or a user running concurrent task add commands), this could be too short, producing spurious failures. Consider making the retry count/interval configurable or at least bumping the defaults (e.g., 30 retries × 100 ms = 3 s).


11-42: Stale lock file is only a risk from SIGKILL or system crash, not from normal signal handling.

The defer unlock() at line 56 of cmd/task/main.go is properly in place. In Go, deferred functions execute when the program exits normally or on SIGINT/SIGTERM, so Ctrl+C will not leave a stale lock. However, a hard kill (kill -9 / SIGKILL) or system crash will bypass cleanup and leave tasks.lock indefinitely, blocking all future invocations.

If robustness against forceful termination is desired, consider:

  1. Write the PID to the lock file, then check on acquisition failure if that PID is still alive and remove stale locks automatically.
  2. Use syscall.Flock (Unix) / LockFileEx (Windows), which are automatically released by the OS when the process exits, regardless of termination method.

Option 2 is the most robust cross-platform solution and eliminates the entire stale-lock risk.

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.

Fix: Implement file locking to prevent data races.

1 participant