mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 19:33:41 +00:00
fix(hls): snapshot state in Store and publish fully initialized runs
Address the completion review findings on top of the upstream review fixes: - Store runs on the engine's checkpoint goroutine without the task lock and serializes the returned value afterwards: take the fetcher mutex and return a deep copy instead of the live state pointer, matching the HTTP protocol's Store pattern - Load the journal and per-run base counters before publishing the run as active, so Stats/Progress never observe half-initialized fields - Bind the journal fingerprint to the key IV as well: a rotated IV on an otherwise identical plan would silently decrypt to garbage - Keep every EXT-X-MEDIA member per group instead of only the first one, so a group with a URI on a later member is still detected - After a merge that raced with a pause, keep the staged segments and exit silently instead of cleaning them up; drop a dead context check and an unused helper, and make the MAP identity and merge part-file names unambiguous
This commit is contained in:
1 parent
f1f17948c6
commit
f9410bbcc7
5 files changed
+79
-54
No files matched your search
@@ -103,8 +103,9 @@ type hlsRun struct {
|
||||
downloaded atomic.Int64
|
||||
doneCount atomic.Int64
|
||||
failCount atomic.Int64
|
||||
baseBytes int64 // stored bytes restored from the journal at startup
|
||||
baseDone int // completed segments restored from the journal at startup
|
||||
baseBytes int64 // stored bytes restored from the journal at startup
|
||||
baseDone int // completed segments restored from the journal at startup
|
||||
pending []int64 // segment indexes to download, set by prepareRun
|
||||
}
|
||||
|
||||
func (r *hlsRun) statsSnapshot() Stats {
|
||||
@@ -431,6 +432,12 @@ func (f *Fetcher) Start() error {
|
||||
if f.client == nil {
|
||||
f.client = f.buildClient()
|
||||
}
|
||||
if f.meta.Res == nil {
|
||||
// A restored plan without a resource cannot determine its output
|
||||
// path; the engine re-resolves when Res is nil.
|
||||
f.mu.Unlock()
|
||||
return errors.New("hls fetcher is not resolved")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
run := &hlsRun{
|
||||
@@ -447,17 +454,19 @@ func (f *Fetcher) Start() error {
|
||||
keyCache: make(map[string][]byte),
|
||||
journal: make(map[int64]int64),
|
||||
}
|
||||
// Prepare staging and the completed-segment counters BEFORE publishing
|
||||
// the run: Stats/Progress may observe it immediately afterwards and must
|
||||
// never read half-initialized plain fields.
|
||||
if err := f.prepareRun(run); err != nil {
|
||||
f.mu.Unlock()
|
||||
run.cancel()
|
||||
close(run.done)
|
||||
return err
|
||||
}
|
||||
f.activeRun = run
|
||||
f.mu.Unlock()
|
||||
|
||||
if err := f.startRun(run); err != nil {
|
||||
// No supervisor was started for this run, so close its done channel
|
||||
// here to release anyone waiting on it.
|
||||
run.cancel()
|
||||
close(run.done)
|
||||
f.clearActiveRun(run)
|
||||
return err
|
||||
}
|
||||
f.launchWorkers(run)
|
||||
go f.supervise(run)
|
||||
return nil
|
||||
}
|
||||
@@ -563,12 +572,14 @@ func (f *Fetcher) supervise(run *hlsRun) {
|
||||
} else {
|
||||
err = f.merge(run)
|
||||
if err == nil {
|
||||
if run.ctx.Err() != nil {
|
||||
// Paused while the merge was in flight: keep the staged
|
||||
// segments and exit silently; the next run re-merges.
|
||||
f.clearActiveRun(run)
|
||||
return
|
||||
}
|
||||
os.RemoveAll(run.tempDir)
|
||||
}
|
||||
if errors.Is(err, context.Canceled) {
|
||||
f.clearActiveRun(run)
|
||||
return
|
||||
}
|
||||
}
|
||||
f.clearActiveRun(run)
|
||||
select {
|
||||
@@ -577,9 +588,11 @@ func (f *Fetcher) supervise(run *hlsRun) {
|
||||
}
|
||||
}
|
||||
|
||||
// startRun prepares the staging folder and launches the segment download
|
||||
// workers. It returns immediately when every segment is already complete.
|
||||
func (f *Fetcher) startRun(run *hlsRun) error {
|
||||
// prepareRun prepares the staging folder and loads the completed-segment
|
||||
// journal into run.baseBytes/baseDone/pending. It performs no goroutine
|
||||
// launches and no context-bound work, so it runs safely under f.mu before
|
||||
// the run is published.
|
||||
func (f *Fetcher) prepareRun(run *hlsRun) error {
|
||||
if err := os.MkdirAll(run.tempDir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -587,8 +600,6 @@ func (f *Fetcher) startRun(run *hlsRun) error {
|
||||
if err := loadJournal(run); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pending []int64
|
||||
for i := range run.segs {
|
||||
if size, ok := run.journal[int64(i)]; ok {
|
||||
if info, err := os.Stat(segmentPath(run.tempDir, int64(i))); err == nil && info.Size() == size {
|
||||
@@ -597,19 +608,25 @@ func (f *Fetcher) startRun(run *hlsRun) error {
|
||||
continue
|
||||
}
|
||||
}
|
||||
pending = append(pending, int64(i))
|
||||
run.pending = append(run.pending, int64(i))
|
||||
}
|
||||
run.downloaded.Store(run.baseBytes)
|
||||
if len(pending) == 0 {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// launchWorkers starts the queue producer and the segment download workers
|
||||
// for a published run. Every goroutine registers itself in run.wg so Pause
|
||||
// waits for all of them.
|
||||
func (f *Fetcher) launchWorkers(run *hlsRun) {
|
||||
if len(run.pending) == 0 {
|
||||
return
|
||||
}
|
||||
queue := make(chan int64)
|
||||
run.wg.Add(1)
|
||||
go func() {
|
||||
defer run.wg.Done()
|
||||
defer close(queue)
|
||||
for _, idx := range pending {
|
||||
for _, idx := range run.pending {
|
||||
select {
|
||||
case <-run.ctx.Done():
|
||||
return
|
||||
@@ -618,7 +635,7 @@ func (f *Fetcher) startRun(run *hlsRun) error {
|
||||
}
|
||||
}()
|
||||
|
||||
workers := min(f.config.SegmentConnections, len(pending))
|
||||
workers := min(f.config.SegmentConnections, len(run.pending))
|
||||
for i := 0; i < workers; i++ {
|
||||
run.wg.Add(1)
|
||||
go func() {
|
||||
@@ -641,7 +658,6 @@ func (f *Fetcher) startRun(run *hlsRun) error {
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) downloadSegment(run *hlsRun, idx int64) error {
|
||||
@@ -791,7 +807,7 @@ func (f *Fetcher) merge(run *hlsRun) error {
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
partPath := target + "." + tempDirPrefix + run.stagingID + ".part"
|
||||
partPath := target + tempDirPrefix + run.stagingID + ".part"
|
||||
out, err := os.Create(partPath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in new issue
Block a user