mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-09-30 19:22:09 +00:00
Fixes #1039. Rather than opening and closing the Bolt DB instance constantly, the cache now maintains one open `*bolthold.Store` for its lifetime, allowing GC, cache read, and cache write operations to occur concurrently. The major risk is this change is, "is it safe to use one Bolt instance across goroutines concurrently?" [Bolt does document its concurrency requirements](https://github.com/boltdb/bolt?tab=readme-ov-file#transactions), and an analysis of our DB interactions looks to me like it introduces very little risk. Most of the cache operations perform multiple touches to the database; for example `useCache` performs a read to fetch a cache object, and then an update to set its `UsedAt` timestamp. If we wanted to ensure consistency in these operations, they should use a Bolt ReadWrite transaction -- but concurrent access would just be setting the field to the same value anyway. The `gcCache` is the complex operation where a transaction might be warranted -- but doing so would also cause the same bug that #1039 indicates. I believe it is safe to run without a transaction because it is protected by an application-level mutex (to prevent multiple concurrent GCs), it is the only code that performs deletes from the database -- these should guarantee that all its delete attempts are successful. In the event of unexpected failure to do the DB write, `gcCache` deletes from the storage before deleting from the DB, so it should just attempt to cleanup again next run. <!--start release-notes-assistant--> <!--URL:https://code.forgejo.org/forgejo/runner--> - bug fixes - [PR](https://code.forgejo.org/forgejo/runner/pulls/1040): <!--number 1040 --><!--line 0 --><!--description Zml4OiBhbGxvdyBHQyAmIGNhY2hlIG9wZXJhdGlvbnMgdG8gb3BlcmF0ZSBjb25jdXJyZW50bHk=-->fix: allow GC & cache operations to operate concurrently<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://code.forgejo.org/forgejo/runner/pulls/1040 Reviewed-by: earl-warren <earl-warren@noreply.code.forgejo.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
225 lines
4.6 KiB
Go
225 lines
4.6 KiB
Go
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
|
|
|
package artifactcache
|
|
|
|
import (
|
|
http "net/http"
|
|
|
|
bolthold "github.com/timshannon/bolthold"
|
|
|
|
io "io"
|
|
|
|
mock "github.com/stretchr/testify/mock"
|
|
|
|
time "time"
|
|
)
|
|
|
|
// mockCaches is an autogenerated mock type for the caches type
|
|
type mockCaches struct {
|
|
mock.Mock
|
|
}
|
|
|
|
// close provides a mock function with no fields
|
|
func (_m *mockCaches) close() {
|
|
_m.Called()
|
|
}
|
|
|
|
// commit provides a mock function with given fields: id, size
|
|
func (_m *mockCaches) commit(id uint64, size int64) (int64, error) {
|
|
ret := _m.Called(id, size)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for commit")
|
|
}
|
|
|
|
var r0 int64
|
|
var r1 error
|
|
if rf, ok := ret.Get(0).(func(uint64, int64) (int64, error)); ok {
|
|
return rf(id, size)
|
|
}
|
|
if rf, ok := ret.Get(0).(func(uint64, int64) int64); ok {
|
|
r0 = rf(id, size)
|
|
} else {
|
|
r0 = ret.Get(0).(int64)
|
|
}
|
|
|
|
if rf, ok := ret.Get(1).(func(uint64, int64) error); ok {
|
|
r1 = rf(id, size)
|
|
} else {
|
|
r1 = ret.Error(1)
|
|
}
|
|
|
|
return r0, r1
|
|
}
|
|
|
|
// exist provides a mock function with given fields: id
|
|
func (_m *mockCaches) exist(id uint64) (bool, error) {
|
|
ret := _m.Called(id)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for exist")
|
|
}
|
|
|
|
var r0 bool
|
|
var r1 error
|
|
if rf, ok := ret.Get(0).(func(uint64) (bool, error)); ok {
|
|
return rf(id)
|
|
}
|
|
if rf, ok := ret.Get(0).(func(uint64) bool); ok {
|
|
r0 = rf(id)
|
|
} else {
|
|
r0 = ret.Get(0).(bool)
|
|
}
|
|
|
|
if rf, ok := ret.Get(1).(func(uint64) error); ok {
|
|
r1 = rf(id)
|
|
} else {
|
|
r1 = ret.Error(1)
|
|
}
|
|
|
|
return r0, r1
|
|
}
|
|
|
|
// gcCache provides a mock function with no fields
|
|
func (_m *mockCaches) gcCache() {
|
|
_m.Called()
|
|
}
|
|
|
|
// getDB provides a mock function with no fields
|
|
func (_m *mockCaches) getDB() *bolthold.Store {
|
|
ret := _m.Called()
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for getDB")
|
|
}
|
|
|
|
var r0 *bolthold.Store
|
|
if rf, ok := ret.Get(0).(func() *bolthold.Store); ok {
|
|
r0 = rf()
|
|
} else {
|
|
if ret.Get(0) != nil {
|
|
r0 = ret.Get(0).(*bolthold.Store)
|
|
}
|
|
}
|
|
|
|
return r0
|
|
}
|
|
|
|
// readCache provides a mock function with given fields: id, repo
|
|
func (_m *mockCaches) readCache(id uint64, repo string) (*Cache, error) {
|
|
ret := _m.Called(id, repo)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for readCache")
|
|
}
|
|
|
|
var r0 *Cache
|
|
var r1 error
|
|
if rf, ok := ret.Get(0).(func(uint64, string) (*Cache, error)); ok {
|
|
return rf(id, repo)
|
|
}
|
|
if rf, ok := ret.Get(0).(func(uint64, string) *Cache); ok {
|
|
r0 = rf(id, repo)
|
|
} else {
|
|
if ret.Get(0) != nil {
|
|
r0 = ret.Get(0).(*Cache)
|
|
}
|
|
}
|
|
|
|
if rf, ok := ret.Get(1).(func(uint64, string) error); ok {
|
|
r1 = rf(id, repo)
|
|
} else {
|
|
r1 = ret.Error(1)
|
|
}
|
|
|
|
return r0, r1
|
|
}
|
|
|
|
// serve provides a mock function with given fields: w, r, id
|
|
func (_m *mockCaches) serve(w http.ResponseWriter, r *http.Request, id uint64) {
|
|
_m.Called(w, r, id)
|
|
}
|
|
|
|
// setgcAt provides a mock function with given fields: at
|
|
func (_m *mockCaches) setgcAt(at time.Time) {
|
|
_m.Called(at)
|
|
}
|
|
|
|
// useCache provides a mock function with given fields: id
|
|
func (_m *mockCaches) useCache(id uint64) error {
|
|
ret := _m.Called(id)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for useCache")
|
|
}
|
|
|
|
var r0 error
|
|
if rf, ok := ret.Get(0).(func(uint64) error); ok {
|
|
r0 = rf(id)
|
|
} else {
|
|
r0 = ret.Error(0)
|
|
}
|
|
|
|
return r0
|
|
}
|
|
|
|
// validateMac provides a mock function with given fields: rundata
|
|
func (_m *mockCaches) validateMac(rundata RunData) (string, error) {
|
|
ret := _m.Called(rundata)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for validateMac")
|
|
}
|
|
|
|
var r0 string
|
|
var r1 error
|
|
if rf, ok := ret.Get(0).(func(RunData) (string, error)); ok {
|
|
return rf(rundata)
|
|
}
|
|
if rf, ok := ret.Get(0).(func(RunData) string); ok {
|
|
r0 = rf(rundata)
|
|
} else {
|
|
r0 = ret.Get(0).(string)
|
|
}
|
|
|
|
if rf, ok := ret.Get(1).(func(RunData) error); ok {
|
|
r1 = rf(rundata)
|
|
} else {
|
|
r1 = ret.Error(1)
|
|
}
|
|
|
|
return r0, r1
|
|
}
|
|
|
|
// write provides a mock function with given fields: id, offset, reader
|
|
func (_m *mockCaches) write(id uint64, offset uint64, reader io.Reader) error {
|
|
ret := _m.Called(id, offset, reader)
|
|
|
|
if len(ret) == 0 {
|
|
panic("no return value specified for write")
|
|
}
|
|
|
|
var r0 error
|
|
if rf, ok := ret.Get(0).(func(uint64, uint64, io.Reader) error); ok {
|
|
r0 = rf(id, offset, reader)
|
|
} else {
|
|
r0 = ret.Error(0)
|
|
}
|
|
|
|
return r0
|
|
}
|
|
|
|
// newMockCaches creates a new instance of mockCaches. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
|
// The first argument is typically a *testing.T value.
|
|
func newMockCaches(t interface {
|
|
mock.TestingT
|
|
Cleanup(func())
|
|
},
|
|
) *mockCaches {
|
|
mock := &mockCaches{}
|
|
mock.Mock.Test(t)
|
|
|
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
|
|
|
return mock
|
|
}
|