Skip to content

Commit d0bc974

Browse files
committed
fix race condition when multiple instance of git clone may get launched at the same time
1 parent 3cffbbf commit d0bc974

8 files changed

Lines changed: 186 additions & 136 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ To reduce the number of calls to the Gitlab api and improve the responsiveness o
109109
While the filesystem lives in memory, the git repositories that are cloned are saved on disk. By default, they are saved in `$XDG_DATA_HOME/gitlabfs` or `$HOME/.local/share/gitlabfs`, if `$XDG_DATA_HOME` is unset. `gitlabfs` symlink to the local clone of that repo. The local clone is unaffected by project rename or archive/unarchive in Gitlab and a given project will always point to the correct local folder.
110110

111111
## Known issues / Future improvements
112-
* There is a race condition that could happen when interacting with git that needs to be fixed.
113-
* Cloning and pulling repositories is currently very resource-intensive, especially on large set of repositories. Need to track down what causes this to happen. For now, leaving `on_clone` set to `init` and `auto_pull` to `false` in the configuration avoids the issue.
112+
* ~~There is a race condition that could happen when interacting with git that needs to be fixed.~~
113+
* ~~Cloning and pulling repositories is currently very resource-intensive, especially on large set of repositories. Need to track down what causes this to happen. For now, leaving `on_clone` set to `init` and `auto_pull` to `false` in the configuration avoids the issue.~~
114114
* Cache persists forever until a manual refresh is requested. Some way to automatically refresh would be nice.
115115
* The filesystem is currently read-only. Implementing `mkdir` to create groups, `ln` or `touch` to create projects, etc. would be nice.
116116
* Code need some cleanup and could maybe be optimized here and there.

config.example.yaml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,8 @@ git:
5252
# The depth of the git history to pull. Set to 0 to pull the full history.
5353
depth: 1
5454

55-
# The number of `git clone` operation that can be queued up
56-
clone_queue_size: 200
55+
# The number of git operations that can be queued up
56+
queue_size: 200
5757

58-
# The number of parallel `git clone` operation that is allowed to run at once
59-
clone_worker_count: 5
60-
61-
# The number of `git pull` operation that can be queued up
62-
pull_queue_size: 500
63-
64-
# The number of parallel `git pull` operation that is allowed to run at once
65-
pull_worker_count: 5
58+
# The number of parallel git operations that is allowed to run at once
59+
worker_count: 5

git/client.go

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package git
22

33
import (
4-
"errors"
4+
"context"
55
"net/url"
66
"os"
77
"path/filepath"
88
"strconv"
9+
"time"
10+
11+
"github.com/vmihailenco/taskq/v3"
12+
"github.com/vmihailenco/taskq/v3/memqueue"
913
)
1014

1115
const (
@@ -25,34 +29,42 @@ type GitClientParam struct {
2529
PullDepth int
2630
AutoPull bool
2731

28-
CloneBuffSize int
29-
CloneWorkerCount int
30-
PullBuffSize int
31-
PullWorkerCount int
32+
QueueSize int
33+
QueueWorkerCount int
3234
}
3335

3436
type gitClient struct {
3537
GitClientParam
36-
cloneChan chan *gitCloneParam
37-
pullChan chan *gitPullParam
38+
queue taskq.Queue
39+
cloneTask *taskq.Task
40+
pullTask *taskq.Task
3841
}
3942

4043
func NewClient(p GitClientParam) (*gitClient, error) {
44+
queueFactory := memqueue.NewFactory()
4145
// Create the client
4246
c := &gitClient{
4347
GitClientParam: p,
44-
cloneChan: make(chan *gitCloneParam, p.CloneBuffSize),
45-
pullChan: make(chan *gitPullParam, p.PullBuffSize),
46-
}
4748

48-
// Start worker goroutines
49-
for i := 0; i < p.CloneWorkerCount; i++ {
50-
go c.cloneWorker()
51-
}
52-
for i := 0; i < p.PullWorkerCount; i++ {
53-
go c.pullWorker()
49+
queue: queueFactory.RegisterQueue(&taskq.QueueOptions{
50+
Name: "git-queue",
51+
MaxNumWorker: int32(p.QueueWorkerCount),
52+
BufferSize: p.QueueSize,
53+
Storage: taskq.NewLocalStorage(),
54+
}),
5455
}
5556

57+
c.cloneTask = taskq.RegisterTask(&taskq.TaskOptions{
58+
Name: "git-clone",
59+
Handler: c.clone,
60+
RetryLimit: 1,
61+
})
62+
c.pullTask = taskq.RegisterTask(&taskq.TaskOptions{
63+
Name: "git-pull",
64+
Handler: c.pull,
65+
RetryLimit: 1,
66+
})
67+
5668
return c, nil
5769
}
5870

@@ -62,28 +74,16 @@ func (c *gitClient) getLocalRepoLoc(pid int) string {
6274

6375
func (c *gitClient) CloneOrPull(url string, pid int, defaultBranch string) (localRepoLoc string, err error) {
6476
localRepoLoc = c.getLocalRepoLoc(pid)
65-
// TODO: Better manage concurrency, filter out duplicate requests
6677
if _, err := os.Stat(localRepoLoc); os.IsNotExist(err) {
67-
// Dispatch to clone worker
68-
select {
69-
case c.cloneChan <- &gitCloneParam{
70-
url: url,
71-
defaultBranch: defaultBranch,
72-
dst: localRepoLoc,
73-
}:
74-
default:
75-
return localRepoLoc, errors.New("failed to clone local repo")
76-
}
78+
// Dispatch clone msg
79+
msg := c.cloneTask.WithArgs(context.Background(), url, defaultBranch, localRepoLoc)
80+
msg.OnceInPeriod(time.Second, pid)
81+
c.queue.Add(msg)
7782
} else if c.AutoPull {
78-
// Dispatch to pull worker
79-
select {
80-
case c.pullChan <- &gitPullParam{
81-
repoPath: localRepoLoc,
82-
defaultBranch: defaultBranch,
83-
}:
84-
default:
85-
return localRepoLoc, errors.New("failed to pull local repo")
86-
}
83+
// Dispatch pull msg
84+
msg := c.pullTask.WithArgs(context.Background(), localRepoLoc, defaultBranch)
85+
msg.OnceInPeriod(time.Second, pid)
86+
c.queue.Add(msg)
8787
}
8888
return localRepoLoc, nil
8989
}

git/clone.go

Lines changed: 19 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,84 +2,65 @@ package git
22

33
import (
44
"fmt"
5-
"os"
65
"strconv"
76

87
"github.com/badjware/gitlabfs/utils"
98
)
109

11-
type gitCloneParam struct {
12-
url string
13-
defaultBranch string
14-
dst string
15-
}
16-
17-
func (c *gitClient) cloneWorker() {
18-
fmt.Println("Started git cloner worker routine")
19-
20-
for gcp := range c.cloneChan {
21-
if _, err := os.Stat(gcp.dst); os.IsNotExist(err) {
22-
if err := c.clone(gcp); err != nil {
23-
fmt.Println(err)
24-
}
25-
}
26-
}
27-
}
28-
29-
func (c *gitClient) clone(gcp *gitCloneParam) error {
10+
func (c *gitClient) clone(url string, defaultBranch string, dst string) error {
3011
if c.CloneMethod == CloneInit {
3112
// "Fake" cloning the repo by never actually talking to the git server
3213
// This skip a fetch operation that we would do if we where to do a proper clone
3314
// We can save a lot of time and network i/o doing it this way, at the cost of
3415
// resulting in a very barebone local copy
3516

3617
// Init the local repo
37-
fmt.Printf("Initializing %v into %v\n", gcp.url, gcp.dst)
18+
fmt.Printf("Initializing %v into %v\n", url, dst)
3819
_, err := utils.ExecProcess(
3920
"git", "init",
40-
"--initial-branch", gcp.defaultBranch,
21+
"--initial-branch", defaultBranch,
4122
"--",
42-
gcp.dst, // directory
23+
dst, // directory
4324
)
4425
if err != nil {
45-
return fmt.Errorf("failed to init git repo %v to %v: %v", gcp.url, gcp.dst, err)
26+
return fmt.Errorf("failed to init git repo %v to %v: %v", url, dst, err)
4627
}
4728

4829
// Configure the remote
4930
_, err = utils.ExecProcessInDir(
50-
gcp.dst, // workdir
31+
dst, // workdir
5132
"git", "remote", "add",
52-
"-m", gcp.defaultBranch,
33+
"-m", defaultBranch,
5334
"--",
5435
c.RemoteName, // name
55-
gcp.url, // url
36+
url, // url
5637
)
5738
if err != nil {
58-
return fmt.Errorf("failed to setup remote %v in git repo %v: %v", gcp.url, gcp.dst, err)
39+
return fmt.Errorf("failed to setup remote %v in git repo %v: %v", url, dst, err)
5940
}
6041

6142
// Configure the default branch
6243
_, err = utils.ExecProcessInDir(
63-
gcp.dst, // workdir
44+
dst, // workdir
6445
"git", "config", "--local",
6546
"--",
66-
fmt.Sprintf("branch.%s.remote", gcp.defaultBranch), // key
47+
fmt.Sprintf("branch.%s.remote", defaultBranch), // key
6748
c.RemoteName, // value
6849

6950
)
7051
if err != nil {
71-
return fmt.Errorf("failed to setup default branch remote in git repo %v: %v", gcp.dst, err)
52+
return fmt.Errorf("failed to setup default branch remote in git repo %v: %v", dst, err)
7253
}
7354
_, err = utils.ExecProcessInDir(
74-
gcp.dst, // workdir
55+
dst, // workdir
7556
"git", "config", "--local",
7657
"--",
77-
fmt.Sprintf("branch.%s.merge", gcp.defaultBranch), // key
78-
fmt.Sprintf("refs/heads/%s", gcp.defaultBranch), // value
58+
fmt.Sprintf("branch.%s.merge", defaultBranch), // key
59+
fmt.Sprintf("refs/heads/%s", defaultBranch), // value
7960

8061
)
8162
if err != nil {
82-
return fmt.Errorf("failed to setup default branch merge in git repo %v: %v", gcp.dst, err)
63+
return fmt.Errorf("failed to setup default branch merge in git repo %v: %v", dst, err)
8364
}
8465
} else {
8566
// Clone the repo
@@ -88,11 +69,11 @@ func (c *gitClient) clone(gcp *gitCloneParam) error {
8869
"--origin", c.RemoteName,
8970
"--depth", strconv.Itoa(c.PullDepth),
9071
"--",
91-
gcp.url, // repository
92-
gcp.dst, // directory
72+
url, // repository
73+
dst, // directory
9374
)
9475
if err != nil {
95-
return fmt.Errorf("failed to clone git repo %v to %v: %v", gcp.url, gcp.dst, err)
76+
return fmt.Errorf("failed to clone git repo %v to %v: %v", url, dst, err)
9677
}
9778
}
9879
return nil

git/pull.go

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,47 +7,32 @@ import (
77
"github.com/badjware/gitlabfs/utils"
88
)
99

10-
type gitPullParam struct {
11-
repoPath string
12-
defaultBranch string
13-
}
14-
15-
func (c *gitClient) pullWorker() {
16-
fmt.Println("Started git puller worker routine")
17-
18-
for gpp := range c.pullChan {
19-
if err := c.pull(gpp); err != nil {
20-
fmt.Println(err)
21-
}
22-
}
23-
}
24-
25-
func (c *gitClient) pull(gpp *gitPullParam) error {
10+
func (c *gitClient) pull(repoPath string, defaultBranch string) error {
2611
// Check if the local repo is on default branch
2712
branchName, err := utils.ExecProcessInDir(
28-
gpp.repoPath, // workdir
13+
repoPath, // workdir
2914
"git", "branch",
3015
"--show-current",
3116
)
3217
if err != nil {
33-
return fmt.Errorf("failed to retrieve HEAD of git repo %v: %v", gpp.repoPath, err)
18+
return fmt.Errorf("failed to retrieve HEAD of git repo %v: %v", repoPath, err)
3419
}
3520

36-
if branchName == gpp.defaultBranch {
21+
if branchName == defaultBranch {
3722
// Pull the repo
3823
_, err = utils.ExecProcessInDir(
39-
gpp.repoPath, // workdir
24+
repoPath, // workdir
4025
"git", "pull",
4126
"--depth", strconv.Itoa(c.PullDepth),
4227
"--",
43-
c.RemoteName, // repository
44-
gpp.defaultBranch, // refspec
28+
c.RemoteName, // repository
29+
defaultBranch, // refspec
4530
)
4631
if err != nil {
47-
return fmt.Errorf("failed to pull git repo %v: %v", gpp.repoPath, err)
32+
return fmt.Errorf("failed to pull git repo %v: %v", repoPath, err)
4833
}
4934
} else {
50-
fmt.Printf("%v != %v, skipping pull", branchName, gpp.defaultBranch)
35+
fmt.Printf("%v != %v, skipping pull", branchName, defaultBranch)
5136
}
5237

5338
return nil

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ module github.com/badjware/gitlabfs
33
go 1.15
44

55
require (
6-
github.com/golang/protobuf v1.5.1 // indirect
6+
github.com/bsm/redislock v0.7.2 // indirect
77
github.com/google/go-querystring v1.1.0 // indirect
88
github.com/hanwen/go-fuse/v2 v2.1.0
99
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
1010
github.com/hashicorp/go-retryablehttp v0.6.8 // indirect
11+
github.com/klauspost/compress v1.14.4 // indirect
12+
github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d
1113
github.com/xanzy/go-gitlab v0.47.0
12-
golang.org/x/net v0.0.0-20210323141857-08027d57d8cf // indirect
1314
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect
14-
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect
1515
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
1616
google.golang.org/appengine v1.6.7 // indirect
1717
gopkg.in/yaml.v2 v2.4.0

0 commit comments

Comments
 (0)