refac: atomic merge, lost-update race, RunPurge cost, duplicated walks
This commit is contained in:
1 parent
0fc3511e2b
commit
074944c99a
45 files changed
+2204
-267
No files matched your search
@@ -0,0 +1,160 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"RenewCMS/internal/domain/transaction"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
)
|
||||
|
||||
type mergeSnapshotter interface {
|
||||
mergeSnapshot() func()
|
||||
}
|
||||
|
||||
func (m *mockImageRepo) mergeSnapshot() func() {
|
||||
saved, nextID := maps.Clone(m.images), m.nextID
|
||||
return func() { m.images, m.nextID = maps.Clone(saved), nextID }
|
||||
}
|
||||
|
||||
func (m *mockArticleRepo) mergeSnapshot() func() {
|
||||
saved := maps.Clone(m.articles)
|
||||
return func() { m.articles = maps.Clone(saved) }
|
||||
}
|
||||
|
||||
func (m *mockSiteRepo) mergeSnapshot() func() {
|
||||
saved := maps.Clone(m.sites)
|
||||
return func() { m.sites = maps.Clone(saved) }
|
||||
}
|
||||
|
||||
func (m *mockAuthorRepo) mergeSnapshot() func() {
|
||||
saved := maps.Clone(m.authors)
|
||||
return func() { m.authors = maps.Clone(saved) }
|
||||
}
|
||||
|
||||
func (m *mockEntryRepo) mergeSnapshot() func() {
|
||||
saved := maps.Clone(m.entries)
|
||||
return func() { m.entries = maps.Clone(saved) }
|
||||
}
|
||||
|
||||
type mockMergeTransactor struct {
|
||||
repos transaction.Repositories
|
||||
failBegin error
|
||||
failCommit error
|
||||
runCalls int
|
||||
committed bool
|
||||
afterCommit func()
|
||||
}
|
||||
|
||||
var _ transaction.Transactor = &mockMergeTransactor{}
|
||||
|
||||
func newMergeTransactor(images image.Repository, articles article.Repository, sites site.Repository, authors author.Repository, entries entry.Repository) *mockMergeTransactor {
|
||||
return &mockMergeTransactor{repos: transaction.Repositories{Articles: articles, Sites: sites, Authors: authors, Entries: entries, Images: images}}
|
||||
}
|
||||
|
||||
func (m *mockMergeTransactor) snapshots() []func() {
|
||||
var restores []func()
|
||||
for _, repo := range []any{m.repos.Articles, m.repos.Sites, m.repos.Authors, m.repos.Entries, m.repos.Images} {
|
||||
if snapshotter, ok := repo.(mergeSnapshotter); ok {
|
||||
restores = append(restores, snapshotter.mergeSnapshot())
|
||||
}
|
||||
}
|
||||
return restores
|
||||
}
|
||||
|
||||
func restoreAll(restores []func()) {
|
||||
for _, restore := range restores {
|
||||
restore()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockMergeTransactor) Run(ctx context.Context, work func(transaction.Repositories) error) (err error) {
|
||||
m.runCalls++
|
||||
m.committed = false
|
||||
if m.failBegin != nil {
|
||||
return m.failBegin
|
||||
}
|
||||
restores := m.snapshots()
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
restoreAll(restores)
|
||||
panic(recovered)
|
||||
}
|
||||
}()
|
||||
if err := work(m.repos); err != nil {
|
||||
restoreAll(restores)
|
||||
return err
|
||||
}
|
||||
if m.failCommit != nil {
|
||||
restoreAll(restores)
|
||||
return m.failCommit
|
||||
}
|
||||
m.committed = true
|
||||
if m.afterCommit != nil {
|
||||
m.afterCommit()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type mergeFileCall struct {
|
||||
name string
|
||||
committed bool
|
||||
}
|
||||
|
||||
type mergeRecordingStorage struct {
|
||||
*mockImageStorage
|
||||
transactor *mockMergeTransactor
|
||||
calls []mergeFileCall
|
||||
failFor map[string]error
|
||||
}
|
||||
|
||||
var _ image.Storage = &mergeRecordingStorage{}
|
||||
|
||||
func newMergeRecordingStorage(transactor *mockMergeTransactor) *mergeRecordingStorage {
|
||||
return &mergeRecordingStorage{mockImageStorage: newMockImageStorage(), transactor: transactor, failFor: map[string]error{}}
|
||||
}
|
||||
|
||||
func (s *mergeRecordingStorage) Delete(name string) error {
|
||||
s.calls = append(s.calls, mergeFileCall{name: name, committed: s.transactor.committed})
|
||||
if err, armed := s.failFor[name]; armed {
|
||||
return err
|
||||
}
|
||||
return s.mockImageStorage.Delete(name)
|
||||
}
|
||||
|
||||
type mergeRecordingVariantFiles struct {
|
||||
*mockVariantFileRepo
|
||||
transactor *mockMergeTransactor
|
||||
calls []mergeFileCall
|
||||
failSource map[string]error
|
||||
}
|
||||
|
||||
var _ image.VariantFileRepository = &mergeRecordingVariantFiles{}
|
||||
|
||||
func newMergeRecordingVariantFiles(transactor *mockMergeTransactor) *mergeRecordingVariantFiles {
|
||||
return &mergeRecordingVariantFiles{mockVariantFileRepo: newMockVariantFileRepo(), transactor: transactor, failSource: map[string]error{}}
|
||||
}
|
||||
|
||||
func (v *mergeRecordingVariantFiles) ListBySource(sourceName string) ([]image.VariantFile, error) {
|
||||
if err, armed := v.failSource[sourceName]; armed {
|
||||
return nil, err
|
||||
}
|
||||
return v.mockVariantFileRepo.ListBySource(sourceName)
|
||||
}
|
||||
|
||||
func (v *mergeRecordingVariantFiles) Delete(id uint32) error {
|
||||
v.calls = append(v.calls, mergeFileCall{name: v.rows[id].SourceName, committed: v.transactor.committed})
|
||||
return v.mockVariantFileRepo.Delete(id)
|
||||
}
|
||||
|
||||
func newTestMergeMedia(images image.Repository, articles article.Repository, storage image.Storage, variantFiles image.VariantFileRepository, recorder audit.Recorder, sites site.Repository, authors author.Repository, types contenttype.Repository, entries entry.Repository) *useCases.MergeMediaUseCase {
|
||||
return useCases.NewMergeMediaUseCase(images, articles, storage, variantFiles, recorder, types, entries, newMergeTransactor(images, articles, sites, authors, entries), log.New(io.Discard, "", 0))
|
||||
}
|
||||
Reference in new issue
Block a user