- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и несуществующая дают один ответ; правило просмотра файлов сужено им же - приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается - удаление учётной записи с записями отвергается стражем, и вешает его сама сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
99 lines
4.1 KiB
Go
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
|
|
}
|