Implemented shared S3 audio caching for prepare and restore --include-audio
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/audio"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -173,10 +174,13 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
registerConfigInput(cfgFile.kind, cfgFile.dst, checksum, cfgFile.source)
|
||||
}
|
||||
|
||||
var audioCacheStats s3AudioMaterializationStats
|
||||
if useS3Audio {
|
||||
if err := materializeS3AudioInputs(ctx, env, m, sessionID, &inputs); err != nil {
|
||||
stats, err := materializeS3AudioInputs(ctx, env, m, sessionID, &inputs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare: materialize s3 audio: %w", err)
|
||||
}
|
||||
audioCacheStats = stats
|
||||
} else {
|
||||
if err := materializeLocalAudioInputs(env, paths, resolvedLocalAudio, registerInput); err != nil {
|
||||
return nil, fmt.Errorf("prepare: %w", err)
|
||||
@@ -211,6 +215,11 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
"inputs_count": len(inputs),
|
||||
"audio_files_resolved": countAudioInputs(inputs),
|
||||
}
|
||||
if useS3Audio {
|
||||
metadata["audio_cache_hits"] = audioCacheStats.CacheHits
|
||||
metadata["audio_cache_misses"] = audioCacheStats.CacheMisses
|
||||
metadata["audio_s3_downloads"] = audioCacheStats.Downloads
|
||||
}
|
||||
if len(previousRequirements) > 0 {
|
||||
metadata["previous_requirements_count"] = len(previousRequirements)
|
||||
if previousHydration != nil {
|
||||
@@ -345,28 +354,34 @@ func materializeLocalAudioInputs(env *Env, paths artifacts.SessionPaths, resolve
|
||||
return nil
|
||||
}
|
||||
|
||||
func materializeS3AudioInputs(ctx context.Context, env *Env, m *manifest.Manifest, sessionID string, inputs *[]manifest.InputRecord) error {
|
||||
type s3AudioMaterializationStats struct {
|
||||
CacheHits int
|
||||
CacheMisses int
|
||||
Downloads int
|
||||
}
|
||||
|
||||
func materializeS3AudioInputs(ctx context.Context, env *Env, m *manifest.Manifest, sessionID string, inputs *[]manifest.InputRecord) (s3AudioMaterializationStats, error) {
|
||||
if env.ObjectStore == nil {
|
||||
return fmt.Errorf("s3 audio input requires object store backend")
|
||||
return s3AudioMaterializationStats{}, fmt.Errorf("s3 audio input requires object store backend")
|
||||
}
|
||||
if env.Config == nil || env.Config.Pipeline == nil || env.Config.Session == nil || env.Config.Pipeline.Storage.S3 == nil || env.Config.Session.Inputs.AudioS3 == nil {
|
||||
return fmt.Errorf("s3 audio input requires pipeline.storage.s3 and session.inputs.audio_s3 configuration")
|
||||
return s3AudioMaterializationStats{}, fmt.Errorf("s3 audio input requires pipeline.storage.s3 and session.inputs.audio_s3 configuration")
|
||||
}
|
||||
|
||||
campaign := strings.TrimSpace(env.Config.Session.Campaign)
|
||||
if campaign == "" {
|
||||
return fmt.Errorf("session campaign is required for s3 audio input")
|
||||
return s3AudioMaterializationStats{}, fmt.Errorf("session campaign is required for s3 audio input")
|
||||
}
|
||||
runID := strings.TrimSpace(m.RunID)
|
||||
if runID == "" {
|
||||
return fmt.Errorf("run id is required for s3 audio input")
|
||||
return s3AudioMaterializationStats{}, fmt.Errorf("run id is required for s3 audio input")
|
||||
}
|
||||
|
||||
sessionPrefix := artifacts.S3SessionPrefix(env.Config.Pipeline.Storage.S3.RootPrefix, campaign, sessionID)
|
||||
audioPrefix := artifacts.S3AudioPrefix(sessionPrefix, env.Config.Session.Inputs.AudioS3.Prefix)
|
||||
objects, err := env.ObjectStore.List(ctx, audioPrefix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list s3 audio objects under %q: %w", audioPrefix, err)
|
||||
return s3AudioMaterializationStats{}, fmt.Errorf("list s3 audio objects under %q: %w", audioPrefix, err)
|
||||
}
|
||||
|
||||
audioObjects := make([]storage.ObjectInfo, 0, len(objects))
|
||||
@@ -384,7 +399,7 @@ func materializeS3AudioInputs(ctx context.Context, env *Env, m *manifest.Manifes
|
||||
return audioObjects[i].Key < audioObjects[j].Key
|
||||
})
|
||||
if len(audioObjects) == 0 {
|
||||
return fmt.Errorf("no .flac files found under s3 audio prefix %q", audioPrefix)
|
||||
return s3AudioMaterializationStats{}, fmt.Errorf("no .flac files found under s3 audio prefix %q", audioPrefix)
|
||||
}
|
||||
|
||||
spoolAudioDir := strings.TrimSpace(m.LocalSpoolDir)
|
||||
@@ -394,45 +409,60 @@ func materializeS3AudioInputs(ctx context.Context, env *Env, m *manifest.Manifes
|
||||
workAudioDir := filepath.Join(pathsWorkDirForManifest(env, m, sessionID), "audio")
|
||||
|
||||
if err := os.MkdirAll(spoolAudioDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create spool audio directory %q: %w", spoolAudioDir, err)
|
||||
return s3AudioMaterializationStats{}, fmt.Errorf("create spool audio directory %q: %w", spoolAudioDir, err)
|
||||
}
|
||||
if err := os.MkdirAll(workAudioDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create work audio directory %q: %w", workAudioDir, err)
|
||||
return s3AudioMaterializationStats{}, fmt.Errorf("create work audio directory %q: %w", workAudioDir, err)
|
||||
}
|
||||
|
||||
seenBase := map[string]string{}
|
||||
stats := s3AudioMaterializationStats{}
|
||||
cacheEnabled := env.Config.Pipeline.Cache.S3Audio == nil || *env.Config.Pipeline.Cache.S3Audio
|
||||
for _, obj := range audioObjects {
|
||||
base := path.Base(obj.Key)
|
||||
if prev, exists := seenBase[base]; exists && prev != obj.Key {
|
||||
return fmt.Errorf("duplicate s3 audio basename %q from %q and %q", base, prev, obj.Key)
|
||||
return s3AudioMaterializationStats{}, fmt.Errorf("duplicate s3 audio basename %q from %q and %q", base, prev, obj.Key)
|
||||
}
|
||||
seenBase[base] = obj.Key
|
||||
|
||||
spoolPath := filepath.Join(spoolAudioDir, base)
|
||||
if err := env.ObjectStore.Download(ctx, obj.Key, spoolPath); err != nil {
|
||||
return fmt.Errorf("download s3 audio object %q: %w", obj.Key, err)
|
||||
}
|
||||
|
||||
workPath := filepath.Join(workAudioDir, base)
|
||||
checksum, err := copyFileIfChanged(env.ArtifactStore, spoolPath, workPath)
|
||||
result, err := audio.MaterializeS3Audio(ctx, audio.S3MaterializeRequest{
|
||||
Store: env.ObjectStore,
|
||||
Object: obj,
|
||||
Bucket: strings.TrimSpace(env.Config.Pipeline.Storage.S3.Bucket),
|
||||
CacheRoot: strings.TrimSpace(env.Config.Pipeline.Cache.Root),
|
||||
CacheEnabled: cacheEnabled,
|
||||
SpoolPath: spoolPath,
|
||||
DestPath: workPath,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("materialize downloaded audio %q: %w", base, err)
|
||||
return s3AudioMaterializationStats{}, err
|
||||
}
|
||||
if result.CacheHit {
|
||||
stats.CacheHits++
|
||||
} else {
|
||||
stats.CacheMisses++
|
||||
}
|
||||
if result.Downloaded {
|
||||
stats.Downloads++
|
||||
}
|
||||
|
||||
*inputs = append(*inputs, manifest.InputRecord{
|
||||
Kind: "audio",
|
||||
Path: workPath,
|
||||
Checksum: checksum,
|
||||
Checksum: result.Checksum,
|
||||
Source: "s3",
|
||||
S3Bucket: strings.TrimSpace(env.Config.Pipeline.Storage.S3.Bucket),
|
||||
S3Key: obj.Key,
|
||||
S3Size: obj.Size,
|
||||
S3ETag: obj.ETag,
|
||||
SpoolPath: spoolPath,
|
||||
SpoolPath: result.SpoolPath,
|
||||
CachePath: result.CachePath,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func countAudioInputs(inputs []manifest.InputRecord) int {
|
||||
|
||||
Reference in New Issue
Block a user