refactor(scanner): consolidate split-exception wiring and drop hot-path lock

ArtistSplitExceptionsRx is called per tag mapping per scanned file across
concurrent goroutines; replace the mutex+joined-key cache with an atomic
pointer compared via slices.Equal. Route all participant call sites through
WithParticipantExceptions and a shared splitParticipantValues helper.
This commit is contained in:
Deluan 2026-07-02 00:34:50 -04:00
parent b3a3d3b2f1
commit 3b41bab94c
2 changed files with 29 additions and 30 deletions

View File

@ -94,15 +94,11 @@ func (md Metadata) processPerformers(participants model.Participants, rolesMbzId
roleIdx[role] = 0
}
conf := model.TagRolesConf()
conf.ExceptionsRx = model.ArtistSplitExceptionsRx()
conf := model.TagRolesConf().WithParticipantExceptions(model.TagPerformer)
titleCaser := cases.Title(language.Und)
for _, performer := range md.Pairs(model.TagPerformer) {
subRole := titleCaser.String(performer.Key())
names := []string{performer.Value()}
if len(conf.Split) > 0 {
names = filterDuplicatedOrEmptyValues(conf.SplitTagValue(names))
}
names := splitParticipantValues(conf, []string{performer.Value()})
for _, name := range names {
artist := model.Artist{
ID: md.artistID(name),
@ -177,6 +173,16 @@ func (md Metadata) buildArtists(names, sorts, mbids []string) []model.Artist {
return artists
}
// splitParticipantValues splits values by the conf separators, dropping
// duplicated or empty entries. Values are returned unchanged when the conf
// has no separators.
func splitParticipantValues(conf model.TagConf, values []string) []string {
if len(conf.Split) == 0 {
return values
}
return filterDuplicatedOrEmptyValues(conf.SplitTagValue(values))
}
// getRoleValues returns the values of a role tag, splitting them if necessary
func (md Metadata) getRoleValues(role model.TagName) []string {
values := md.Strings(role)
@ -187,12 +193,8 @@ func (md Metadata) getRoleValues(role model.TagName) []string {
if conf.Split == nil {
conf = model.TagRolesConf()
}
if len(conf.Split) > 0 {
conf.ExceptionsRx = model.ArtistSplitExceptionsRx()
values = conf.SplitTagValue(values)
return filterDuplicatedOrEmptyValues(values)
}
return values
conf = conf.WithParticipantExceptions(role)
return splitParticipantValues(conf, values)
}
// getArtistValues returns the values of a single or multi artist tag, splitting them if necessary
@ -209,12 +211,8 @@ func (md Metadata) getArtistValues(single, multi model.TagName) []string {
if conf.Split == nil {
conf = model.TagArtistsConf()
}
if len(conf.Split) > 0 {
conf.ExceptionsRx = model.ArtistSplitExceptionsRx()
vSingle = conf.SplitTagValue(vSingle)
return filterDuplicatedOrEmptyValues(vSingle)
}
return vSingle
conf = conf.WithParticipantExceptions(single)
return splitParticipantValues(conf, vSingle)
}
func (md Metadata) mapDisplayName(singularTagName, pluralTagName model.TagName) string {

View File

@ -7,6 +7,7 @@ import (
"slices"
"strings"
"sync"
"sync/atomic"
"unicode"
"unicode/utf8"
@ -129,25 +130,25 @@ func compileExceptionsRegex(exceptions []string) *regexp.Regexp {
return rx
}
var artistSplitExceptions struct {
sync.Mutex
key string
rx *regexp.Regexp
type artistSplitExceptionsCache struct {
names []string
rx *regexp.Regexp
}
var artistSplitExceptions atomic.Pointer[artistSplitExceptionsCache]
// ArtistSplitExceptionsRx returns the regex for Scanner.ArtistSplitExceptions,
// or nil if none are configured. Compiled lazily (config hooks only run once
// per process, before tests can override the option) and cached until the
// configured list changes.
// configured list changes. Lock-free on the cache-hit path, as this is called
// per tag mapping per scanned file, across concurrent scanner goroutines.
func ArtistSplitExceptionsRx() *regexp.Regexp {
c := &artistSplitExceptions
c.Lock()
defer c.Unlock()
key := strings.Join(conf.Server.Scanner.ArtistSplitExceptions, "\x00")
if c.key != key {
c.key = key
c.rx = compileExceptionsRegex(conf.Server.Scanner.ArtistSplitExceptions)
names := conf.Server.Scanner.ArtistSplitExceptions
if c := artistSplitExceptions.Load(); c != nil && slices.Equal(c.names, names) {
return c.rx
}
c := &artistSplitExceptionsCache{names: slices.Clone(names), rx: compileExceptionsRegex(names)}
artistSplitExceptions.Store(c)
return c.rx
}