Files
transcriber/internal/entity/job.go
T
av 8af8ec2e54 у записи появился владелец: чужую больше не отдают
- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы
  `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и
  несуществующая дают один ответ; правило просмотра файлов сужено им же
- приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи
  пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный
  файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается
- удаление учётной записи с записями отвергается стражем, и вешает его сама
  сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
2026-08-14 12:18:11 +03:00

99 lines
4.1 KiB
Go

package entity
import (
"time"
"git.vakhrushev.me/av/transcriber/internal/clock"
)
type TranscribeJob struct {
Id string
State string
// OwnerID — учётная запись, от имени которой запись принята. Пуст у записей
// из Telegram: связи чата с учётной записью сервис не ведёт. Назначается
// один раз, при приёме, и у записи, где он есть, больше не меняется.
OwnerID *string
Source string
FileID *string
ErrorText *string
AcquisitionID *string
AcquireTime *time.Time
DelayTime *time.Time
Attempts int // Число попыток: растёт при захвате, обнуляется на шаге без отказа
RecognitionOpID *string // ID операции распознавания в Yandex Cloud
TranscriptionText *string // Результат распознавания
TgChatId *int64 // Telegram: в какой чат отправить результат распознавания
TgReplyMessageId *int // Telegram: с каким сообщением связать результат распознавания
CreatedAt time.Time
UpdatedAt time.Time
}
const (
StateCreated = "created"
StateConverted = "converted"
StateTranscribe = "transcribe"
StateDone = "done"
StateFailed = "failed"
// StateDead — задача, которую мы повторяли и перестали. От `failed` она
// отличается тем, чей это приговор: в `failed` задачу переводит шаг,
// рассудивший об этой записи окончательно, а сюда она уходит без такого
// суждения. Ни один шаг конвейера в неё не переводит сам.
StateDead = "dead"
)
const (
SourceUnknown = "unknown"
SourceApi = "api"
SourceTelegram = "telegram"
)
// Переводит задачу в новое состояние, при этом очищает все
// служебные поля предыдущего состояния, как-то время задержки, информацию о воркере и тд
func (j *TranscribeJob) MoveToState(state string) {
j.State = state
j.DelayTime = nil
j.AcquisitionID = nil
j.AcquireTime = nil
// Шаг, дошедший до перехода, завершился без отказа, а попытки считают
// именно отказавшие: иначе задача, прошедшая конвейер целиком, накопила бы
// их поштучно и умерла бы здоровой.
j.Attempts = 0
j.UpdatedAt = clock.Now()
}
func (j *TranscribeJob) MoveToStateAndDelay(state string, delay *time.Time) {
j.MoveToState(state)
j.DelayTime = delay
j.UpdatedAt = clock.Now()
}
func (j *TranscribeJob) Done(transcriptionText string) {
j.MoveToState(StateDone)
j.TranscriptionText = &transcriptionText
}
func (j *TranscribeJob) Fail(errText string) {
j.MoveToState(StateFailed)
j.ErrorText = &errText
}
// RetryAfter освобождает отказавшую задачу для повтора: захват снимается,
// пауза ставится, а число попыток сохраняется — по нему растёт пауза и
// наступает предел.
func (j *TranscribeJob) RetryAfter(delay time.Time) {
j.AcquisitionID = nil
j.AcquireTime = nil
j.DelayTime = &delay
j.UpdatedAt = clock.Now()
}
// Die переводит задачу, исчерпавшую попытки, в состояние «мертва». Число
// попыток при этом сохраняется: по нему видно, сколько раз мы пробовали, а
// возвращает задачу в работу владелец правкой состояния.
func (j *TranscribeJob) Die(errText string) {
attempts := j.Attempts
j.MoveToState(StateDead)
j.Attempts = attempts
j.ErrorText = &errText
}