Skip to content

Make mlc pull repo thread safe - #265

Open
arjunsuresh with Copilot wants to merge 13 commits into
mainfrom
copilot/fix-mlc-pull-repo-thread-safety
Open

Make mlc pull repo thread safe#265
arjunsuresh with Copilot wants to merge 13 commits into
mainfrom
copilot/fix-mlc-pull-repo-thread-safety

Conversation

Copilot AI commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Concurrent mlc pull repo calls race on repos.json — two threads can both read, both modify their local copy, and one write overwrites the other's changes. Similarly, two threads can both see a repo directory is absent and race to git clone the same path.

Changes

mlc/repo_action.py

  • Import: Added from filelock import FileLock, Timeout (already a transitive dep via index.py)
  • register_repo: Wraps repos.json read-modify-write in FileLock(repos.json.lock, timeout=60), eliminating lost-update races on concurrent registration
  • unregister_repo: Same FileLock protection for the remove-and-write path
  • pull_repo: Wraps the entire clone/pull/register section in a per-repo FileLock(repo_path + ".lock", timeout=300), preventing double-clone races when two callers simultaneously find the repo absent

tests/test_pull_repo_thread_safety.py

  • test_concurrent_register_repo_no_data_loss: 10 threads concurrently register unique paths → all 10 present in repos.json, no duplicates
  • test_concurrent_unregister_repo_no_data_loss: 10 threads concurrently unregister unique paths → all 10 removed, no duplicates

✅ PR Checklist

  • Target branch is dev

📌 Note: PRs must be raised against dev. Do not commit directly to main.

✅ Testing & CI

  • Have tested the changes in my local environment, else have properly conveyed in the PR description
  • The change includes a GitHub Action to test the script(if it is possible to be added).
  • No existing GitHub Actions are failing because of this change.

📚 Documentation

  • README or help docs are updated for new features or changes.
  • CLI help messages are meaningful and complete.

📁 File Hygiene & Output Handling

  • No unintended files (e.g., logs, cache, temp files, pycache, output folders) are committed.

🛡️ Safety & Security

  • No secrets or credentials are committed.
  • Paths, shell commands, and environment handling are safe and portable.

🙌 Contribution Hygiene

  • PR title and description are concise and clearly state the purpose of the change.
  • All reviewer feedback has been addressed.

Copilot AI linked an issue Jul 18, 2026 that may be closed by this pull request
Copilot AI changed the title [WIP] Make mlc pull repo thread safe Make mlc pull repo thread safe Jul 18, 2026
Copilot AI requested a review from arjunsuresh July 18, 2026 20:28
@github-actions

Copy link
Copy Markdown

🤖 AI PR Review Summary\n\nThis PR introduces file locking around critical file and directory operations to prevent concurrent access issues. It adds FileLock usage for repos.json read/write and for cloning or pulling git repositories. The design improves concurrency safety but adds complexity and potential deadlock risks if locks are not managed carefully. The stash/apply logic for force pulling with local changes is enhanced with better error handling and rollback. Overall, the changes improve robustness but require careful testing to avoid lock contention or git state inconsistencies.

@github-actions

Copy link
Copy Markdown

MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅

@github-actions

Copy link
Copy Markdown

🤖 AI PR Review Summary\n\nThis PR introduces file locking around critical file operations to prevent concurrent access issues and adds locking around git repository cloning and pulling to avoid race conditions. It also refactors the force pull logic to stash local changes before pulling and attempts to reapply them after, with improved error handling and rollback on stash apply conflicts. The main risks involve potential deadlocks or lock contention due to long lock hold times during git operations, and complexity in stash conflict recovery. The design improves concurrency safety but increases complexity in error handling and recovery.

@arjunsuresh
arjunsuresh requested a review from anandhu-eng July 18, 2026 21:38

@anandhu-eng anandhu-eng 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.

🤖 Claude Code Review (requested by @anandhusooraj)

Reviewed by Claude (claude-sonnet-4-6) at the request of a maintainer.


Overview

The race conditions described are real: concurrent pull_repo callers can both find the repo directory absent and race to git clone the same path; concurrent register_repo / unregister_repo callers can corrupt repos.json via a lost-update. The FileLock approach using an existing transitive dependency is the right tool. Lock ordering is consistent (repo.lock always acquired before repos.json.lock), so no deadlock risk.


Issues

🔴 The register_repo thread-safety test does not test RepoAction.register_repo

test_concurrent_register_repo_no_data_loss calls self._register_path(path) — a private helper defined in the test class that re-implements the FileLock pattern itself. It never calls RepoAction.register_repo at all.

def _register_path(self, path):
    """Thread-safe repos.json registration (mirrors register_repo logic)."""
    repos_lock_file = self.repos_file + ".lock"
    with FileLock(repos_lock_file, timeout=60):
        ...

If someone removes FileLock from RepoAction.register_repo, this test will still pass. The test validates that FileLock works in general, not that register_repo uses it. The unregister test is correct — it calls the real unregister_repo function.

Fix: Call RepoAction.register_repo (or use RepoAction.pull_repo end-to-end with mocked git) in the register test, the same way the unregister test calls the real function.

🔴 No test for the double-clone race condition

The PR description identifies the double-clone race as the primary problem (two threads can both see a repo directory is absent and race to git clone). There is no test that exercises this path. The two tests cover register/unregister only, not the pull_repo clone guard.

A minimal test: spawn N threads all calling pull_repo for the same (absent) repo URL with git mocked; assert git clone is called exactly once.

🟡 Timeout is imported but never caught explicitly

from filelock import FileLock, Timeout

When FileLock exhausts its timeout it raises filelock.Timeout. Neither pull_repo nor register_repo / unregister_repo catches it specifically — it falls through to the generic except Exception as e handler, which returns:

"Error pulling repository: [Timeout was raised because the lock ... could not be acquired]"

This is confusing for users. The Timeout import suggests the intent was to handle it, but the handler was never written.

Fix: Catch Timeout explicitly and return a clear, actionable message:

except Timeout:
    return {
        'return': 1,
        'error': (
            f"Could not acquire lock for {repo_path} after 300 seconds. "
            "Another mlc process may be cloning or pulling this repo. "
            "Try again once the other operation completes."
        )
    }

Similarly for the repos.json lock in register_repo / unregister_repo.

🟢 Lock files are left on disk next to repo directories

FileLock creates <repo_path>.lock (e.g. mlcommons@mlperf-automations.lock) next to each repo directory in the repos folder, and never removes them. The files are harmless — repo listing reads from repos.json, not by scanning the directory. But users who manually browse ~/MLC/repos/ will see unfamiliar .lock files alongside their repos.

This is a cosmetic issue with no functional impact, but worth documenting in a comment near the lock construction so future maintainers don't mistake them for corruption.

🟢 PR template checklist vs documented branch policy

The PR checklist item Target branch is 'dev' is unchecked, and this PR correctly targets main. AGENTS.md states PRs should target main. The checklist template appears to be outdated — it's a template issue, not a code issue, but it's causing friction across PRs.


Summary

Area Status
Core lock design (FileLock, lock ordering) ✅ Correct
register_repo lock ✅ Correct implementation
unregister_repo lock ✅ Correct implementation
pull_repo clone-race lock ✅ Correct implementation
register_repo thread-safety test ❌ Tests a re-implementation, not the real method
Double-clone race test ❌ Missing entirely
Timeout handling ⚠️ Imported but never caught explicitly
Lock file litter 🟢 Cosmetic only

The locking logic itself is sound. The two test gaps are the main things to fix before merging.

@arjunsuresh

Copy link
Copy Markdown
Contributor

@copilot address the issue in the last comment

Copilot AI commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Claude Code Review (requested by @anandhusooraj)

Reviewed by Claude (claude-sonnet-4-6) at the request of a maintainer....

Addressed all the issues raised:

  1. register_repo test now calls the real methodtest_concurrent_register_repo_no_data_loss creates real temp directories with proper meta.yaml files (unique UIDs) and calls RepoAction.register_repo directly. If the FileLock were removed from register_repo, the test would detect data loss and fail.

  2. Double-clone race test addedtest_concurrent_pull_repo_clone_called_once spawns N threads all calling pull_repo for the same absent repo URL with subprocess.run mocked. It asserts git clone is invoked exactly once.

  3. Timeout now caught explicitly in pull_repo, register_repo, and unregister_repo, each returning a clear, actionable error message.

  4. Comments added near the FileLock constructions in all three locations noting that the .lock files are left on disk intentionally and are harmless.

Commit: the latest two commits on this PR.

@arjunsuresh
arjunsuresh marked this pull request as ready for review July 19, 2026 10:28
@arjunsuresh
arjunsuresh requested a review from a team as a code owner July 19, 2026 10:28
@github-actions

Copy link
Copy Markdown

🤖 AI PR Review Summary\n\nThis PR introduces file locking using the filelock library to prevent concurrent modifications of critical files and directories during repo operations. It adds locking around updates to repos.json and around cloning/pulling git repositories. The design improves robustness against race conditions but introduces potential blocking and timeout errors. The force pull logic is enhanced to stash local changes before pulling and restore them after, with error handling for stash apply conflicts. Risks include deadlocks if locks are not released properly and complexity in error recovery during stash conflicts.

@github-actions

Copy link
Copy Markdown

🤖 AI PR Review Summary\n\nThis PR introduces file locking around critical file and directory operations to prevent concurrent access issues. It adds FileLock usage when reading/writing repos.json and when cloning or pulling git repositories. The locking improves robustness in multi-process scenarios but adds complexity and potential for deadlocks or timeouts. The git pull logic is enhanced to handle local changes with a forced stash and apply workflow, improving safety but increasing complexity and error handling paths. Overall, the design improves concurrency safety but requires careful testing of lock acquisition and stash conflict recovery.

@arjunsuresh
arjunsuresh changed the base branch from main to dev July 19, 2026 10:41
@arjunsuresh

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Resolved. The conflict was between the --shallow/--depth/--extra_git_args features added to origin/dev and the FileLock thread safety changes in this branch. I've merged both sets of changes in mlc/repo_action.py — the new features from dev are preserved, and the FileLock wrapping in pull_repo, register_repo, and unregister_repo is retained. All 10 tests pass. Commit: c8aa6f1

@arjunsuresh

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Resolved. The branch now includes all changes from the latest origin/dev (commits ed5706e and f1a71ba: extra_git_args validation, installer venv improvements) alongside the FileLock thread safety changes. All 10 tests pass.

@arjunsuresh

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request - there are 3 more conflicts w.r.t the dev branch

Copilot AI commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request - there are 3 more conflicts w.r.t the dev branch

Resolved. Took origin/dev as the base for mlc/repo_action.py (which had the most recent features: extra_git_args validation, force pull improvements, apptainer support) and re-applied the FileLock thread safety wrapping on top:

  • FileLock import and Timeout handling preserved in all three locations (register_repo, unregister_repo, pull_repo)
  • All latest dev features retained
  • All 3 thread safety tests pass

Commit: the latest merge commit on this PR.

@github-actions

Copy link
Copy Markdown

🤖 AI PR Review Summary\n\nThis PR introduces file locking to prevent concurrent modifications of critical files and directories during repository operations. It adds a FileLock around the repos.json update to avoid race conditions when multiple processes modify the repo list, and similarly adds a FileLock around cloning and pulling operations on individual repositories to ensure exclusive access. The force pull logic with stash handling is preserved but now protected by the lock. Risks include potential deadlocks or delays if locks are held too long, and the lock files remain on disk but are harmless. The design improves concurrency safety but adds complexity and potential timeout errors that are handled with user-friendly messages.

@github-actions

Copy link
Copy Markdown

🤖 AI PR Review Summary\n\nThis PR introduces file locking around critical file and directory operations to prevent concurrent modifications, specifically when updating repos.json and when cloning or pulling git repositories. It uses the filelock library to acquire locks with timeouts, improving robustness in multi-process scenarios. The design improves concurrency safety but introduces risks of deadlocks or delays if locks are held too long. The force pull logic with git stash is preserved but now wrapped inside the repo lock. Some error handling and logging are enhanced. Overall, the changes are positive but require careful testing under concurrent usage to avoid lock contention issues.

@anandhu-eng anandhu-eng 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.

[Analysis by claude]

The locking itself is correct — I verified filelock 3.13.1 release semantics empirically rather than trusting the docs:

Failure while the holder has the lock Result
Exception inside the with __exit__ calls release() unconditionally → lock freed, waiter proceeds immediately
Hard kill (SIGKILL / OOM / power loss) fcntl.flock is bound to the fd, so the kernel drops it on process death — no stale-lock deadlock; the leftover .lock file is empty and unheld
Timeout Handled by the new except Timeout: branches; the waiter corrupts nothing

So the PR does what it says. The remaining issues are that the lock gives mutual exclusion but not atomicity — there is no rollback of partial on-disk state, and the next process misreads that state as healthy. Details inline; the clone-rollback one (:584-597 + :611) is the one I'd consider blocking, because I reproduced it leaving the repo permanently unpullable.

Out of diff range, same failure mode: add_repo at repo_action.py:275 does os.makedirs(repo_path) and then register_repo with no repo-level lock, so two concurrent mlc add repo on the same path still race (FileExistsError). The PR title scopes to pull repo, so this is a note rather than a request.

tests/test_pull_repo_thread_safety.py: test_concurrent_pull_repo_clone_called_once is itself not thread-safe — each of the 5 threads runs with patch('mlc.repo_action.subprocess.run', ...), and unittest.mock.patch mutates a module global. When the first thread exits its with, it restores the real subprocess.run while the other four are still inside pull_repo, which can shell out to real git against https://github.com/example/test-repo.git. Apply the patch once in the main thread before starting the threads. Separately, assertEqual(errors, []) asserts nothing useful here, because pull_repo returns error dicts rather than raising — threads 2–5 almost certainly return {'return': 1} and the test stays green. Assert on the returned dicts instead. (Also, the PR description lists 2 tests; the file has 3.)

Comment thread mlc/repo_action.py Outdated
Comment on lines +584 to +597
if not os.path.exists(repo_path):
logger.info(
f"Cloning repository {repo_url} to {repo_path}...")

else:
logger.info(
f"Repository {repo_name} already exists at {repo_path}. Checking for local changes...")

# Check for local changes
status_command = [
'git',
'-C',
repo_path,
'status',
'--porcelain',
'--untracked-files=no']
local_changes = subprocess.run(
status_command, capture_output=True, text=True)

if local_changes.stdout.strip():
if not force:
logger.warning(
"There are local changes in the repository. Please commit or stash them before checking out.")
print(local_changes.stdout.strip())
return {
"return": 0, "warning": f"Local changes detected in the already existing repository: {repo_path}, skipping the pull"}
# Build clone command
clone_command = ['git', 'clone']
if branch:
clone_command += ['--branch', branch]
if clone_depth is not None:
clone_command += ['--depth', str(clone_depth)]
clone_command += extra_args
clone_command += [repo_url, repo_path]

logger.warning(
"Local changes detected. Running force pull with temporary git stash.")
stash_created = False
try:
stash_before = subprocess.run(
['git', '-C', repo_path, 'stash', 'list'],
capture_output=True,
text=True,
check=True
)
stash_res = subprocess.run(
['git', '-C', repo_path, 'stash', 'push',
'-m', 'mlc pull repo --force'],
capture_output=True,
text=True,
check=True
)
stash_after = subprocess.run(
['git', '-C', repo_path, 'stash', 'list'],
capture_output=True,
text=True,
check=True
)
stash_created = len(stash_after.stdout.splitlines()
) > len(stash_before.stdout.splitlines())
except subprocess.CalledProcessError as e:
stash_error = (e.stderr or e.stdout or str(e)).strip()
return {
"return": 1,
"error": f"Force pull failed while stashing local changes in {repo_path}: {stash_error}"
}
subprocess.run(clone_command, check=True)

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.

[Analysis by claude] The lock serialises the clone, but nothing rolls back a partial clone, and the existence check on line 584 then treats the wreckage as a valid repo. I reproduced the full chain:

  1. Process 1 takes the lock, git clone starts, process is killed mid-clone (SIGKILL / OOM / Ctrl-C / dropped link).
  2. The directory survives, containing a .git with a configured origin, branch master, and no HEAD.
  3. Lock is released correctly. Process 2 acquires it, hits if not os.path.exists(repo_path)False → takes the "already exists" branch.
  4. That branch ends in git pull, which fails with fatal: no tracking information for the current branch.

It never recovers — every subsequent mlc pull repo re-enters step 3 and fails identically, so the user has to rm -rf ~/MLC/repos/<repo> by hand. So the answer to "does an error mid-clone lose the lock?" is no, but it does poison the path for everyone afterwards.

Worth noting the boundary: on a clean clone failure (bad URL, auth reject) git removes the directory itself — I confirmed that. The leak is specific to the abrupt-termination paths.

Suggested fix — clone to a sibling temp path and os.rename into place inside the lock, so repo_path is either absent or complete:

tmp_path = repo_path + ".tmp-clone"
shutil.rmtree(tmp_path, ignore_errors=True)
clone_command += [repo_url, tmp_path]
try:
    subprocess.run(clone_command, check=True)
    os.rename(tmp_path, repo_path)
except BaseException:      # BaseException so KeyboardInterrupt cleans up too
    shutil.rmtree(tmp_path, ignore_errors=True)
    raise

Comment thread mlc/repo_action.py
Comment on lines +604 to +615
status_command = [
'git',
'-C',
repo_path,
'status',
'--porcelain',
'--untracked-files=no']
local_changes = subprocess.run(
status_command, capture_output=True, text=True)

if local_changes.stdout.strip():
if not force:

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.

[Analysis by claude] Paired with the clone-rollback issue above: this branch cannot distinguish a healthy checkout from a half-cloned one, and it silently picks the wrong recovery.

local_changes.returncode is never inspected. On the partial clone from an interrupted git clone, git status --porcelain --untracked-files=no returns rc=0 with empty stdout (verified). So local_changes.stdout.strip() is falsy, control falls to the else at line 726 which logs "No local changes detected. Pulling latest changes...", and git pull then fails on a repo that should simply have been re-cloned.

The same blind spot applies to a directory that exists but isn't a git repo at all — rc=128, stderr populated, stdout empty, so it reads as "clean" too.

Even with the temp-dir+rename fix, a cheap guard here is worth having: validate with git -C repo_path rev-parse HEAD (or check returncode) and treat a broken/absent checkout as re-clone rather than pull.

Comment thread mlc/repo_action.py Outdated
logger.info(f"Added new repo path: {repo_path}")

with open(repos_file_path, 'w') as f:
json.dump(repos_list, f, indent=2)

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.

[Analysis by claude] The lock closes the writer-vs-writer race, but the write is still non-atomic and readers never take this lock, so the race just moves.

open(..., 'w') truncates and json.dump rewrites incrementally. Meanwhile Action.load_repos_and_meta() (mlc/action.py:95) and Action.load_repos() (mlc/action.py:174) both do a bare json.load with no FileLock. A read landing inside that truncate→rewrite window gets a truncated file and raises JSONDecodeError — with repos.json momentarily looking empty, which is the worst-case content to observe.

Writing to a temp file and os.replace-ing it fixes this and makes the existing lock-free readers safe without having to touch action.py:

tmp = repos_file_path + ".tmp"
with open(tmp, 'w') as f:
    json.dump(repos_list, f, indent=2)
os.replace(tmp, repos_file_path)   # atomic on POSIX and Windows

Same applies to the json.dump in unregister_repo at line 1078.

Comment thread mlc/repo_action.py Outdated
# Lock file sits next to the repo directory; left on disk but
# harmless.
repo_lock_file = repo_path + ".lock"
with FileLock(repo_lock_file, timeout=300):

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.

[Analysis by claude] 300 s is likely too short for the workload this guards, and timing out is currently a hard failure.

A cold clone of mlperf-automations on a slow or throttled link comfortably exceeds five minutes, and the large model/dataset repos more so. When that happens the waiter returns {'return': 1} even though the holder is making perfectly good progress — a false failure caused by the fix rather than by any real contention. In CI, where several jobs may warm the same MLC_REPOS, this is the likely first symptom.

Two options, either is fine:

  • On Timeout, re-check os.path.exists(repo_path) (plus a validity check) before erroring — if the other process finished the clone, there is nothing left to do and this can return success.
  • Make the timeout configurable (e.g. MLC_REPO_LOCK_TIMEOUT) and default it substantially higher.

Minor, related: creating the lock file now requires write permission on repos_path itself, not just on the repo directory. On a shared MLC_REPOS across Unix users that surfaces as a PermissionError reported through the generic handler as "Error pulling repository: [Errno 13]", which won't point anyone at the lock file. An explicit branch would help.

Comment thread mlc/repo_action.py
return {"return": 1,
"error": f"Syntax error in {meta_file_path}: {e}"}

r = self.register_repo(

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.

[Analysis by claude] Nit / future-proofing: this introduces nested lock acquisition<repo_path>.lock is held here while register_repo goes on to acquire repos.json.lock (line 334).

There is no deadlock today because the ordering is consistent everywhere (repo lock → repos.json lock), but nothing documents or enforces that invariant. Anyone later adding a path that takes repos.json.lock first and then a repo lock gets a hang bounded only by the 60 s / 300 s timeouts, which is a genuinely unpleasant thing to debug.

A one-line comment stating the ordering rule at both acquisition sites would be enough.

Comment thread mlc/repo_action.py
)
}

self.repos = self.load_repos_and_meta()

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.

[Analysis by claude] Nit: this re-read happens after the lock is released, so it can observe a repos.json that a concurrent process has modified in the interim — meaning self.repos may not reflect the list this call just wrote, and the next(...) lookup on line 356 can miss the repo that was just registered.

Low impact given the caller usually re-derives state anyway, but if the intent is "read back what I wrote", pulling this inside the with block is free and removes the window.

@anandhu-eng
anandhu-eng changed the base branch from dev to main August 14, 2026 06:08
main gained repos.json locking of its own via #267, which touched the
same two functions this branch does, so register_repo and unregister_repo
conflicted four times.

Both sides were semantically identical -- a 60s FileLock over the
repos.json read-modify-write plus an error dict on Timeout -- so the
resolution keeps main's structure and this branch's wording:

- lock path: use main's shared _repos_lock_file() helper rather than this
  branch's inlined repos_file_path + ".lock". The helper already exists in
  the merged file, so inlining would duplicate it.
- Timeout message: keep this branch's longer text, which names the likely
  cause and the remedy. It also drops the stray f-prefix on main's
  placeholder-free f-string.

This branch's own contribution is untouched: the per-repo
FileLock(repo_path + ".lock", timeout=300) around the clone/pull/register
section of pull_repo, and its Timeout handler. That handler sits after
RuntimeError and CalledProcessError but before the bare Exception --
correct, since filelock.Timeout derives from OSError and so is not
swallowed by either earlier clause.

Verified: no conflict markers, mlc/repo_action.py compiles, full suite
53 passed, and the net diff against main is exactly this branch's
additions. The one autopep8 nit remaining in the file is pre-existing on
main (identical 12-line diff), so it is left alone.
…urious clone path

Co-authored-by: arjunsuresh <4791823+arjunsuresh@users.noreply.github.com>
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.

Make mlc pull repo thread safe

3 participants