待ち行列への登録と状態表示を追加(新着からの登録、/queue、DATABASE_URL)
This commit is contained in:
@@ -121,6 +121,17 @@ func (s *Store) Retry(ctx context.Context, id int64) error {
|
||||
return transitionResult(tag.RowsAffected(), err)
|
||||
}
|
||||
|
||||
// RetryEpisode はエピソード ID で失敗・中断されたジョブを再投入する。
|
||||
func (s *Store) RetryEpisode(ctx context.Context, episodeID string) error {
|
||||
tag, err := s.pool.Exec(ctx, `UPDATE download_jobs SET status='queued', phase=NULL,
|
||||
downloaded_bytes=NULL, total_bytes=NULL, total_is_estimate=false, speed_bps=NULL,
|
||||
eta_seconds=NULL, fragment_index=NULL, fragment_count=NULL, format_id=NULL,
|
||||
error_kind=NULL, error_message=NULL, heartbeat_at=NULL, started_at=NULL,
|
||||
finished_at=NULL, updated_at=now()
|
||||
WHERE episode_id=$1 AND status IN ('failed','canceled')`, episodeID)
|
||||
return transitionResult(tag.RowsAffected(), err)
|
||||
}
|
||||
|
||||
// ClaimNext は最も古い未実行ジョブを同時 worker と重複せず取得する。
|
||||
func (s *Store) ClaimNext(ctx context.Context) (Job, error) {
|
||||
return scanJob(s.pool.QueryRow(ctx, `WITH next AS (
|
||||
@@ -162,6 +173,14 @@ func (s *Store) Cancel(ctx context.Context, id int64) error {
|
||||
return transitionResult(tag.RowsAffected(), err)
|
||||
}
|
||||
|
||||
// CancelQueued は worker 未着手のジョブだけを取り消す。
|
||||
func (s *Store) CancelQueued(ctx context.Context, id int64) error {
|
||||
tag, err := s.pool.Exec(ctx, `UPDATE download_jobs SET status='canceled', phase=NULL,
|
||||
error_kind='canceled', heartbeat_at=NULL, finished_at=now(), updated_at=now()
|
||||
WHERE id=$1 AND status='queued'`, id)
|
||||
return transitionResult(tag.RowsAffected(), err)
|
||||
}
|
||||
|
||||
// RequeueStale は cutoff より前に heartbeat が止まった実行中ジョブを戻す。
|
||||
func (s *Store) RequeueStale(ctx context.Context, cutoff time.Time) (int64, error) {
|
||||
tag, err := s.pool.Exec(ctx, `UPDATE download_jobs SET status='queued', phase=NULL,
|
||||
|
||||
@@ -407,6 +407,89 @@ func TestRejectedTransitions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryEpisode(t *testing.T) {
|
||||
s := New(testPool(t, true))
|
||||
ctx := context.Background()
|
||||
job, _, err := s.Enqueue(ctx, sampleEpisode("retry-by-episode"), "manual")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.RetryEpisode(ctx, job.EpisodeID); !errors.Is(err, ErrInvalidTransition) {
|
||||
t.Fatalf("待機中の再試行: %v", err)
|
||||
}
|
||||
if err := s.Cancel(ctx, job.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.RetryEpisode(ctx, job.EpisodeID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := s.GetJob(ctx, job.ID)
|
||||
if err != nil || got.Status != "queued" || got.ErrorKind != nil || got.FinishedAt != nil {
|
||||
t.Fatalf("エピソード ID による再試行: %+v %v", got, err)
|
||||
}
|
||||
claimed, err := s.ClaimNext(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
downloaded, total := int64(10), int64(20)
|
||||
format := "video-1"
|
||||
if err := s.UpdateProgress(ctx, claimed.ID, Progress{
|
||||
Phase: "video", DownloadedBytes: &downloaded, TotalBytes: &total,
|
||||
TotalIsEstimate: true, FormatID: &format,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Fail(ctx, claimed.ID, "network", "通信失敗"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.RetryEpisode(ctx, job.EpisodeID); err != nil {
|
||||
t.Fatalf("失敗からの再試行: %v", err)
|
||||
}
|
||||
got, err = s.GetJob(ctx, job.ID)
|
||||
if err != nil || got.Status != "queued" || got.Phase != nil || got.DownloadedBytes != nil || got.TotalBytes != nil || got.TotalIsEstimate || got.FormatID != nil || got.ErrorKind != nil || got.ErrorMessage != nil || got.HeartbeatAt != nil || got.StartedAt != nil || got.FinishedAt != nil || got.Attempts != 1 {
|
||||
t.Fatalf("失敗情報の消去: %+v %v", got, err)
|
||||
}
|
||||
if err := s.RetryEpisode(ctx, "unknown"); !errors.Is(err, ErrInvalidTransition) {
|
||||
t.Fatalf("存在しないエピソード: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelQueuedDoesNotCancelRunning(t *testing.T) {
|
||||
s := New(testPool(t, true))
|
||||
ctx := context.Background()
|
||||
queued, _, err := s.Enqueue(ctx, sampleEpisode("queue-cancel"), "manual")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.CancelQueued(ctx, queued.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
canceled, err := s.GetJob(ctx, queued.ID)
|
||||
if err != nil || canceled.Status != "canceled" || canceled.ErrorKind == nil || *canceled.ErrorKind != "canceled" || canceled.FinishedAt == nil {
|
||||
t.Fatalf("待機中の取り消し: %+v %v", canceled, err)
|
||||
}
|
||||
if err := s.CancelQueued(ctx, queued.ID); !errors.Is(err, ErrInvalidTransition) {
|
||||
t.Fatalf("再取り消し: %v", err)
|
||||
}
|
||||
if err := s.CancelQueued(ctx, -1); !errors.Is(err, ErrInvalidTransition) {
|
||||
t.Fatalf("存在しないジョブの取り消し: %v", err)
|
||||
}
|
||||
if err := s.Retry(ctx, queued.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
claimed, err := s.ClaimNext(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.CancelQueued(ctx, claimed.ID); !errors.Is(err, ErrInvalidTransition) {
|
||||
t.Fatalf("実行中の取り消し: %v", err)
|
||||
}
|
||||
job, err := s.GetJob(ctx, claimed.ID)
|
||||
if err != nil || job.Status != "running" {
|
||||
t.Fatalf("実行中の状態: %+v %v", job, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimNextConcurrent(t *testing.T) {
|
||||
s := New(testPool(t, true))
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user