Skip to content

Commit 88da506

Browse files
authored
Add finalizers to ensure that repos are closed and blobreaders are closed (#19495) (#19496)
It may be prudent to add runtime finalizers to the git.Repository and git.blobReader objects to absolutely ensure that these are both properly cancelled, cleaned and closed out. This commit is a backport of an extract from #19448 Signed-off-by: Andrew Thornton <[email protected]>
1 parent 35a7db4 commit 88da506

File tree

3 files changed

+157
-18
lines changed

3 files changed

+157
-18
lines changed

modules/git/blob_nogogit.go

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ import (
1212
"bytes"
1313
"io"
1414
"math"
15+
"runtime"
16+
"sync"
1517

1618
"code.gitea.io/gitea/modules/log"
19+
"code.gitea.io/gitea/modules/process"
1720
)
1821

1922
// Blob represents a Git object.
@@ -54,11 +57,15 @@ func (b *Blob) DataAsync() (io.ReadCloser, error) {
5457
return io.NopCloser(bytes.NewReader(bs)), err
5558
}
5659

57-
return &blobReader{
60+
br := &blobReader{
61+
repo: b.repo,
5862
rd: rd,
5963
n: size,
6064
cancel: cancel,
61-
}, nil
65+
}
66+
runtime.SetFinalizer(br, (*blobReader).finalizer)
67+
68+
return br, nil
6269
}
6370

6471
// Size returns the uncompressed size of the blob
@@ -86,6 +93,10 @@ func (b *Blob) Size() int64 {
8693
}
8794

8895
type blobReader struct {
96+
lock sync.Mutex
97+
closed bool
98+
99+
repo *Repository
89100
rd *bufio.Reader
90101
n int64
91102
cancel func()
@@ -104,27 +115,57 @@ func (b *blobReader) Read(p []byte) (n int, err error) {
104115
}
105116

106117
// Close implements io.Closer
107-
func (b *blobReader) Close() error {
118+
func (b *blobReader) Close() (err error) {
119+
b.lock.Lock()
120+
defer b.lock.Unlock()
121+
if b.closed {
122+
return
123+
}
124+
return b.close()
125+
}
126+
127+
func (b *blobReader) close() (err error) {
108128
defer b.cancel()
129+
b.closed = true
109130
if b.n > 0 {
131+
var n int
110132
for b.n > math.MaxInt32 {
111-
n, err := b.rd.Discard(math.MaxInt32)
133+
n, err = b.rd.Discard(math.MaxInt32)
112134
b.n -= int64(n)
113135
if err != nil {
114-
return err
136+
return
115137
}
116138
b.n -= math.MaxInt32
117139
}
118-
n, err := b.rd.Discard(int(b.n))
140+
n, err = b.rd.Discard(int(b.n))
119141
b.n -= int64(n)
120142
if err != nil {
121-
return err
143+
return
122144
}
123145
}
124146
if b.n == 0 {
125-
_, err := b.rd.Discard(1)
147+
_, err = b.rd.Discard(1)
126148
b.n--
127-
return err
149+
return
150+
}
151+
return
152+
}
153+
154+
func (b *blobReader) finalizer() error {
155+
if b == nil {
156+
return nil
157+
}
158+
b.lock.Lock()
159+
defer b.lock.Unlock()
160+
if b.closed {
161+
return nil
128162
}
129-
return nil
163+
164+
pid := ""
165+
if b.repo.Ctx != nil {
166+
pid = " from PID: " + string(process.GetPID(b.repo.Ctx))
167+
}
168+
log.Error("Finalizer running on unclosed blobReader%s: %s%s", pid, b.repo.Path)
169+
170+
return b.close()
130171
}

modules/git/repo_base_gogit.go

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ import (
1212
"context"
1313
"errors"
1414
"path/filepath"
15+
"runtime"
16+
"sync"
1517

18+
"code.gitea.io/gitea/modules/log"
1619
gitealog "code.gitea.io/gitea/modules/log"
20+
"code.gitea.io/gitea/modules/process"
1721
"code.gitea.io/gitea/modules/setting"
1822

1923
"github.com/go-git/go-billy/v5/osfs"
@@ -28,6 +32,9 @@ type Repository struct {
2832

2933
tagCache *ObjectCache
3034

35+
lock sync.Mutex
36+
closed bool
37+
3138
gogitRepo *gogit.Repository
3239
gogitStorage *filesystem.Storage
3340
gpgSettings *GPGSettings
@@ -63,23 +70,57 @@ func OpenRepositoryCtx(ctx context.Context, repoPath string) (*Repository, error
6370
return nil, err
6471
}
6572

66-
return &Repository{
73+
repo := &Repository{
6774
Path: repoPath,
6875
gogitRepo: gogitRepo,
6976
gogitStorage: storage,
7077
tagCache: newObjectCache(),
7178
Ctx: ctx,
72-
}, nil
79+
}
80+
81+
runtime.SetFinalizer(repo, (*Repository).finalizer)
82+
83+
return repo, nil
7384
}
7485

7586
// Close this repository, in particular close the underlying gogitStorage if this is not nil
76-
func (repo *Repository) Close() {
77-
if repo == nil || repo.gogitStorage == nil {
87+
func (repo *Repository) Close() (err error) {
88+
if repo == nil {
89+
return
90+
}
91+
repo.lock.Lock()
92+
defer repo.lock.Unlock()
93+
return repo.close()
94+
}
95+
96+
func (repo *Repository) close() (err error) {
97+
repo.closed = true
98+
if repo.gogitStorage == nil {
7899
return
79100
}
80-
if err := repo.gogitStorage.Close(); err != nil {
101+
err = repo.gogitStorage.Close()
102+
if err != nil {
81103
gitealog.Error("Error closing storage: %v", err)
82104
}
105+
return
106+
}
107+
108+
func (repo *Repository) finalizer() error {
109+
if repo == nil {
110+
return nil
111+
}
112+
repo.lock.Lock()
113+
defer repo.lock.Unlock()
114+
if !repo.closed {
115+
pid := ""
116+
if repo.Ctx != nil {
117+
pid = " from PID: " + string(process.GetPID(repo.Ctx))
118+
}
119+
log.Error("Finalizer running on unclosed repository%s: %s%s", pid, repo.Path)
120+
}
121+
122+
// We still need to run the close fn as it may be possible to reopen the gogitrepo after close
123+
return repo.close()
83124
}
84125

85126
// GoGitRepo gets the go-git repo representation

modules/git/repo_base_nogogit.go

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ import (
1313
"context"
1414
"errors"
1515
"path/filepath"
16+
"runtime"
17+
"sync"
1618

1719
"code.gitea.io/gitea/modules/log"
20+
"code.gitea.io/gitea/modules/process"
1821
)
1922

2023
// Repository represents a Git repository.
@@ -25,6 +28,10 @@ type Repository struct {
2528

2629
gpgSettings *GPGSettings
2730

31+
lock sync.Mutex
32+
33+
closed bool
34+
2835
batchCancel context.CancelFunc
2936
batchReader *bufio.Reader
3037
batchWriter WriteCloserError
@@ -64,29 +71,57 @@ func OpenRepositoryCtx(ctx context.Context, repoPath string) (*Repository, error
6471
repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repoPath)
6572
repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repo.Path)
6673

74+
runtime.SetFinalizer(repo, (*Repository).finalizer)
75+
6776
return repo, nil
6877
}
6978

7079
// CatFileBatch obtains a CatFileBatch for this repository
7180
func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) {
72-
if repo.batchCancel == nil || repo.batchReader.Buffered() > 0 {
81+
repo.lock.Lock()
82+
defer repo.lock.Unlock()
83+
84+
if repo.closed || repo.batchReader.Buffered() > 0 {
7385
log.Debug("Opening temporary cat file batch for: %s", repo.Path)
7486
return CatFileBatch(ctx, repo.Path)
7587
}
88+
89+
if repo.batchCancel == nil {
90+
repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repo.Path)
91+
}
92+
7693
return repo.batchWriter, repo.batchReader, func() {}
7794
}
7895

7996
// CatFileBatchCheck obtains a CatFileBatchCheck for this repository
8097
func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) {
81-
if repo.checkCancel == nil || repo.checkReader.Buffered() > 0 {
98+
repo.lock.Lock()
99+
defer repo.lock.Unlock()
100+
101+
if repo.closed || repo.checkReader.Buffered() > 0 {
82102
log.Debug("Opening temporary cat file batch-check: %s", repo.Path)
83103
return CatFileBatchCheck(ctx, repo.Path)
84104
}
105+
106+
if repo.checkCancel == nil {
107+
repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repo.Path)
108+
}
109+
85110
return repo.checkWriter, repo.checkReader, func() {}
86111
}
87112

88113
// Close this repository, in particular close the underlying gogitStorage if this is not nil
89-
func (repo *Repository) Close() {
114+
func (repo *Repository) Close() (err error) {
115+
if repo == nil {
116+
return
117+
}
118+
repo.lock.Lock()
119+
defer repo.lock.Unlock()
120+
121+
return repo.close()
122+
}
123+
124+
func (repo *Repository) close() (err error) {
90125
if repo == nil {
91126
return
92127
}
@@ -102,4 +137,26 @@ func (repo *Repository) Close() {
102137
repo.checkReader = nil
103138
repo.checkWriter = nil
104139
}
140+
repo.closed = true
141+
return
142+
}
143+
144+
func (repo *Repository) finalizer() (err error) {
145+
if repo == nil {
146+
return
147+
}
148+
repo.lock.Lock()
149+
defer repo.lock.Unlock()
150+
if repo.closed {
151+
return nil
152+
}
153+
154+
if repo.batchCancel != nil || repo.checkCancel != nil {
155+
pid := ""
156+
if repo.Ctx != nil {
157+
pid = " from PID: " + string(process.GetPID(repo.Ctx))
158+
}
159+
log.Error("Finalizer running on unclosed repository%s: %s%s", pid, repo.Path)
160+
}
161+
return repo.close()
105162
}

0 commit comments

Comments
 (0)