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
@@ -6,6 +6,7 @@ import (
|
||||
"RenewCMS/internal/domain/image"
|
||||
"cmp"
|
||||
"errors"
|
||||
"maps"
|
||||
"slices"
|
||||
)
|
||||
|
||||
@@ -30,16 +31,12 @@ func newMediaResolver(images image.Repository) *mediaResolver {
|
||||
|
||||
func (r *mediaResolver) resolve(owner contenttype.ContentType, record entry.Entry) (map[string]EntryMedia, error) {
|
||||
resolved := map[string]EntryMedia{}
|
||||
for _, field := range contenttype.MediaFields(owner.Fields) {
|
||||
id, set := contenttype.MediaID(record.Data[field.Key])
|
||||
if !set {
|
||||
continue
|
||||
}
|
||||
media, err := r.lookup(id)
|
||||
for _, ref := range contenttype.MediaRefs(owner.Fields, record.Data) {
|
||||
media, err := r.lookup(ref.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved[field.Key] = media
|
||||
resolved[ref.Field.Key] = media
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
@@ -73,6 +70,63 @@ type mediaEntry struct {
|
||||
record entry.Entry
|
||||
}
|
||||
|
||||
func (m mediaEntry) keysPointingAt(ids map[uint32]bool) []string {
|
||||
keys := []string{}
|
||||
for _, ref := range contenttype.MediaRefs(m.owner.Fields, m.record.Data) {
|
||||
if ids[ref.ID] {
|
||||
keys = append(keys, ref.Field.Key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func compareMediaEntries(a, b mediaEntry) int {
|
||||
return cmp.Or(cmp.Compare(a.owner.Name, b.owner.Name), cmp.Compare(a.owner.ID, b.owner.ID), cmp.Compare(a.record.ID, b.record.ID))
|
||||
}
|
||||
|
||||
type entryMediaIndex struct {
|
||||
byImage map[uint32][]mediaEntry
|
||||
}
|
||||
|
||||
func newEntryMediaIndex(found []mediaEntry) *entryMediaIndex {
|
||||
index := &entryMediaIndex{byImage: map[uint32][]mediaEntry{}}
|
||||
for _, candidate := range found {
|
||||
distinct := map[uint32]bool{}
|
||||
for _, ref := range contenttype.MediaRefs(candidate.owner.Fields, candidate.record.Data) {
|
||||
distinct[ref.ID] = true
|
||||
}
|
||||
for id := range distinct {
|
||||
index.byImage[id] = append(index.byImage[id], candidate)
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
func (i *entryMediaIndex) usageCounts() map[uint32]int {
|
||||
counts := make(map[uint32]int, len(i.byImage))
|
||||
for id, found := range i.byImage {
|
||||
counts[id] = len(found)
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
func (i *entryMediaIndex) referencing(imageIDs []uint32) []mediaEntry {
|
||||
type entryKey struct{ typeID, entryID uint32 }
|
||||
seen := map[entryKey]bool{}
|
||||
var found []mediaEntry
|
||||
for _, id := range imageIDs {
|
||||
for _, candidate := range i.byImage[id] {
|
||||
key := entryKey{candidate.owner.ID, candidate.record.ID}
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
found = append(found, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.SortFunc(found, compareMediaEntries)
|
||||
return found
|
||||
}
|
||||
|
||||
type entryMediaScanner struct {
|
||||
contentTypes contenttype.Repository
|
||||
entries entry.Repository
|
||||
@@ -82,7 +136,7 @@ func newEntryMediaScanner(contentTypes contenttype.Repository, entries entry.Rep
|
||||
return &entryMediaScanner{contentTypes: contentTypes, entries: entries}
|
||||
}
|
||||
|
||||
func (s *entryMediaScanner) scan() ([]mediaEntry, error) {
|
||||
func (s *entryMediaScanner) index() (*entryMediaIndex, error) {
|
||||
types, err := s.contentTypes.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -100,91 +154,51 @@ func (s *entryMediaScanner) scan() ([]mediaEntry, error) {
|
||||
found = append(found, mediaEntry{owner: owner, record: record})
|
||||
}
|
||||
}
|
||||
slices.SortFunc(found, func(a, b mediaEntry) int {
|
||||
return cmp.Or(cmp.Compare(a.owner.Name, b.owner.Name), cmp.Compare(a.owner.ID, b.owner.ID), cmp.Compare(a.record.ID, b.record.ID))
|
||||
})
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func (m mediaEntry) keysPointingAt(ids map[uint32]bool) []string {
|
||||
keys := []string{}
|
||||
for _, field := range contenttype.MediaFields(m.owner.Fields) {
|
||||
if id, set := contenttype.MediaID(m.record.Data[field.Key]); set && ids[id] {
|
||||
keys = append(keys, field.Key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
return newEntryMediaIndex(found), nil
|
||||
}
|
||||
|
||||
func (s *entryMediaScanner) usageCounts() (map[uint32]int, error) {
|
||||
found, err := s.scan()
|
||||
index, err := s.index()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
counts := map[uint32]int{}
|
||||
for _, candidate := range found {
|
||||
distinct := map[uint32]bool{}
|
||||
for _, id := range entry.MediaIDs(candidate.owner.Fields, candidate.record.Data) {
|
||||
distinct[id] = true
|
||||
}
|
||||
for id := range distinct {
|
||||
counts[id]++
|
||||
}
|
||||
}
|
||||
return counts, nil
|
||||
return index.usageCounts(), nil
|
||||
}
|
||||
|
||||
func (s *entryMediaScanner) referencing(imageID uint32) ([]mediaEntry, error) {
|
||||
found, err := s.scan()
|
||||
index, err := s.index()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets := map[uint32]bool{imageID: true}
|
||||
return slices.DeleteFunc(found, func(candidate mediaEntry) bool {
|
||||
return len(candidate.keysPointingAt(targets)) == 0
|
||||
}), nil
|
||||
return index.referencing([]uint32{imageID}), nil
|
||||
}
|
||||
|
||||
type entryRepoint struct {
|
||||
original entry.Entry
|
||||
updated entry.Entry
|
||||
}
|
||||
|
||||
func (s *entryMediaScanner) repoint(copyIDs []uint32, keptID uint32) ([]entryRepoint, error) {
|
||||
found, err := s.scan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func repointEntries(entries entry.Repository, candidates []mediaEntry, copyIDs []uint32, keptID uint32) (int, error) {
|
||||
copies := map[uint32]bool{}
|
||||
for _, id := range copyIDs {
|
||||
copies[id] = true
|
||||
}
|
||||
applied := []entryRepoint{}
|
||||
for _, candidate := range found {
|
||||
keys := candidate.keysPointingAt(copies)
|
||||
updated := 0
|
||||
for _, candidate := range candidates {
|
||||
current, err := entries.Get(candidate.owner.ID, candidate.record.ID)
|
||||
if errors.Is(err, entry.ErrEntryNotFound) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return updated, err
|
||||
}
|
||||
keys := mediaEntry{owner: candidate.owner, record: current}.keysPointingAt(copies)
|
||||
if len(keys) == 0 {
|
||||
continue
|
||||
}
|
||||
change := entryRepoint{original: candidate.record, updated: candidate.record}
|
||||
change.updated.Data = make(map[string]any, len(candidate.record.Data))
|
||||
for key, value := range candidate.record.Data {
|
||||
change.updated.Data[key] = value
|
||||
}
|
||||
current.Data = maps.Clone(current.Data)
|
||||
for _, key := range keys {
|
||||
change.updated.Data[key] = float64(keptID)
|
||||
current.Data[key] = float64(keptID)
|
||||
}
|
||||
if _, err := s.entries.Update(change.updated); err != nil {
|
||||
s.restore(applied)
|
||||
return nil, err
|
||||
if _, err := entries.Update(current); err != nil {
|
||||
return updated, err
|
||||
}
|
||||
applied = append(applied, change)
|
||||
}
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
func (s *entryMediaScanner) restore(applied []entryRepoint) {
|
||||
for _, change := range applied {
|
||||
_, err := s.entries.Update(change.original)
|
||||
logCompensation("restore entry media", err)
|
||||
updated++
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
Reference in new issue
Block a user