Отмена доходит до внешнего собеседника, а токен не покидает единой точки
- контекст проложен от воркера и обоих входов до внешних вызовов: ffmpeg и ffprobe заводятся через exec.CommandContext, SpeechKit и Object Storage принимают ctx вместо context.Background, скачивание записи идёт запросом с контекстом. Прежде остановка сервиса не доходила до чужой работы вовсе - прерванный шаг приговора не выносит: убитый по контексту ffmpeg отдаёт «signal: killed», от настоящего отказа неотличимо ни типом, ни errors.Is, и различает их только ctx.Err(). Задача остаётся на повтор, попытку не тратит и отправителю о несуществующем сбое не сообщает; воркер не считает остановку отказом, а задача не забирается вовсе, если нас уже остановили - клиента Bot API заводит единая точка internal/adapter/telegram: токен стоит в пути каждого обращения, а http.Client кладёт адрес в *url.Error целиком. Чистка на месте употребления закрывала один вызов из пяти — теперь свой Do чистит отказ, подменённый логгер вычищает токен из строк самой библиотеки, а транспорт бота токена не получает вовсе - принятие операции распознавания защищено от отмены своим пределом: SpeechKit мог её принять и начать считать деньги, а потерянный идентификатор заставил бы повтор оплатить ту же запись второй раз - приём по HTTP доводит запись до задачи независимо от отправителя: на контексте запроса один обрыв соединения терял полностью загруженную запись - ответ Telegram с не-2xx кодом больше не становится записью: прежде тело отказа доезжало до хранилища и умирало на ffprobe, уводя диагностику
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
@@ -29,19 +30,19 @@ import (
|
||||
// failingConverter отказывает на каждой попытке.
|
||||
type failingConverter struct{}
|
||||
|
||||
func (c *failingConverter) Convert(string, string) error {
|
||||
func (c *failingConverter) Convert(context.Context, string, string) error {
|
||||
return errors.New("конвертация не удалась")
|
||||
}
|
||||
|
||||
type okMetaViewer struct{}
|
||||
|
||||
func (m *okMetaViewer) GetInfo(string) (*contract.AudioInfo, error) {
|
||||
func (m *okMetaViewer) GetInfo(context.Context, string) (*contract.AudioInfo, error) {
|
||||
return &contract.AudioInfo{Seconds: 1}, nil
|
||||
}
|
||||
|
||||
type failingMetaViewer struct{}
|
||||
|
||||
func (m *failingMetaViewer) GetInfo(string) (*contract.AudioInfo, error) {
|
||||
func (m *failingMetaViewer) GetInfo(context.Context, string) (*contract.AudioInfo, error) {
|
||||
return nil, errors.New("запись не читается")
|
||||
}
|
||||
|
||||
@@ -101,7 +102,7 @@ func newTelegramJob(t *testing.T, env *pipelineEnv) *entity.TranscribeJob {
|
||||
t.Helper()
|
||||
|
||||
chatId := int64(100)
|
||||
job, err := env.service.CreateJobFromTelegram(strings.NewReader("запись"), "voice.ogg", chatId, 1)
|
||||
job, err := env.service.CreateJobFromTelegram(t.Context(), strings.NewReader("запись"), "voice.ogg", chatId, 1)
|
||||
require.NoError(t, err)
|
||||
return job
|
||||
}
|
||||
@@ -149,7 +150,7 @@ func TestJobDiesAfterAttemptLimit(t *testing.T) {
|
||||
rotAcquisition(t, env, job.Id)
|
||||
|
||||
// Следующий захват видит перебор и хоронит задачу.
|
||||
err := env.service.FindAndRunConversionJob()
|
||||
err := env.service.FindAndRunConversionJob(t.Context())
|
||||
|
||||
var noop *contract.NoopJobError
|
||||
require.ErrorAs(t, err, &noop, "мёртвая задача шагу не отдаётся")
|
||||
@@ -178,7 +179,7 @@ func TestDeadJobReturnsAfterStateEdit(t *testing.T) {
|
||||
_, err := env.jobRepo.FindAndAcquire(entity.StateCreated, "holder", time.Now().Add(time.Hour))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Error(t, env.service.FindAndRunConversionJob())
|
||||
require.Error(t, env.service.FindAndRunConversionJob(t.Context()))
|
||||
|
||||
record, err := env.app.FindRecordById(migrations.JobsCollection, job.Id)
|
||||
require.NoError(t, err)
|
||||
@@ -209,7 +210,7 @@ func TestFailedStepSchedulesRetryWithGrowingDelay(t *testing.T) {
|
||||
require.NoError(t, env.app.Save(record))
|
||||
|
||||
// Первый отказ.
|
||||
require.Error(t, env.service.FindAndRunConversionJob())
|
||||
require.Error(t, env.service.FindAndRunConversionJob(t.Context()))
|
||||
|
||||
after, err := env.jobRepo.GetByID(job.Id)
|
||||
require.NoError(t, err)
|
||||
@@ -220,7 +221,7 @@ func TestFailedStepSchedulesRetryWithGrowingDelay(t *testing.T) {
|
||||
|
||||
// Второй отказ — с той же задачи, пауза снята вручную.
|
||||
clearDelay(t, env, job.Id)
|
||||
require.Error(t, env.service.FindAndRunConversionJob())
|
||||
require.Error(t, env.service.FindAndRunConversionJob(t.Context()))
|
||||
|
||||
after, err = env.jobRepo.GetByID(job.Id)
|
||||
require.NoError(t, err)
|
||||
@@ -247,7 +248,7 @@ func TestWorkFileRemovedAfterIntakeFailure(t *testing.T) {
|
||||
|
||||
env := newPipelineEnv(t, &failingMetaViewer{}, &failingConverter{})
|
||||
|
||||
_, err := env.service.CreateJobFromApi(strings.NewReader("запись"), "sample.mp3")
|
||||
_, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "sample.mp3")
|
||||
require.Error(t, err, "отказ источника метаданных роняет приём")
|
||||
|
||||
leftovers, err := filepath.Glob(filepath.Join(tempDir, "transcriber-*"))
|
||||
@@ -263,7 +264,7 @@ func TestWorkFileRemovedAfterSuccessfulIntake(t *testing.T) {
|
||||
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
|
||||
|
||||
_, err := env.service.CreateJobFromApi(strings.NewReader("запись"), "sample.mp3")
|
||||
_, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "sample.mp3")
|
||||
require.NoError(t, err)
|
||||
|
||||
leftovers, err := filepath.Glob(filepath.Join(tempDir, "transcriber-*"))
|
||||
@@ -280,7 +281,7 @@ func TestJobNeverPointsToMissingFile(t *testing.T) {
|
||||
|
||||
// Конвертация отказывает — задача уходит в `failed`, но ссылка остаётся на
|
||||
// исходную запись, а не на несозданный результат.
|
||||
require.NoError(t, env.service.FindAndRunConversionJob())
|
||||
require.NoError(t, env.service.FindAndRunConversionJob(t.Context()))
|
||||
|
||||
after, err := env.jobRepo.GetByID(job.Id)
|
||||
require.NoError(t, err)
|
||||
@@ -298,7 +299,7 @@ func TestStoredContentSurvivesRoundTrip(t *testing.T) {
|
||||
|
||||
content := strings.Repeat("запись ", 1000)
|
||||
|
||||
job, err := env.service.CreateJobFromApi(strings.NewReader(content), "sample.mp3")
|
||||
job, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader(content), "sample.mp3")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job.FileID)
|
||||
|
||||
@@ -321,7 +322,7 @@ func TestStoredContentSurvivesRoundTrip(t *testing.T) {
|
||||
func TestLocalizeGivesReadableCopy(t *testing.T) {
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
|
||||
|
||||
job, err := env.service.CreateJobFromApi(strings.NewReader("содержимое"), "sample.mp3")
|
||||
job, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("содержимое"), "sample.mp3")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job.FileID)
|
||||
|
||||
@@ -355,7 +356,7 @@ func TestWorkFilesRemovedAfterConversionFailure(t *testing.T) {
|
||||
require.Empty(t, leftovers, "приём убрал свою рабочую копию")
|
||||
|
||||
// Конвертация отказывает — задача уходит в `failed`, копии убраны.
|
||||
require.NoError(t, env.service.FindAndRunConversionJob())
|
||||
require.NoError(t, env.service.FindAndRunConversionJob(t.Context()))
|
||||
|
||||
leftovers, err = filepath.Glob(filepath.Join(tempDir, "transcriber-*"))
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user