Go: Повтор не создаёт второго предупреждения — задача с проверкой
Практическая задача на Go по теме «Повтор не создаёт второго предупреждения». Условие содержит контракт функций, предоставленные типы и требования к результату. Используйте выданный шаблон и сохраняйте его публичные объявления. Перед проверкой разберите успешный путь, обработку ошибок и отмену операции, когда она предусмотрена контрактом. Здесь оценивается конкретный компонент сервиса; в полном курсе микросервисов на Go эти компоненты связываются с HTTP, PostgreSQL, gRPC и Kafka.
В этой задаче вы реализуете одну функцию, которая делает обработку события логически однократной при доставке at-least-once. Kafka может передать одно и то же сообщение более одного раза: например, если процесс упал после записи в базу данных, но до подтверждения offset. Повторная доставка не означает нового измерения: она возможна, например, после падения до подтверждённой фиксации offset. Сам факт повтора не доказывает, какой checkpoint сохранил брокер.
Разбор payload и вычисление предупреждения уже выполнены до вызова вашей функции. Вы получаете готовый Alert и непустой eventID, который совпадает с alert.SourceEventID. Ваша задача — оркестрировать одну локальную транзакцию, которая записывает уникальный marker и Alert вместе, и вернуть один из трёх исходов.
Интерфейсы
Harness предоставляет следующие типы; вы не объявляете их сами.
type Outcome uint8
const (
OutcomeUnknown Outcome = iota
OutcomeApplied
OutcomeAlreadyApplied
)
type Alert struct {
SourceEventID string
DeviceID string
Message string
}
type Store interface {
Begin(context.Context) (Tx, error)
}
type Tx interface {
TryMark(context.Context, string) (bool, error)
SaveAlert(context.Context, Alert) error
Commit() error
Rollback()
}Семантика методов Tx
- TryMark(ctx, eventID) возвращает
true, если уникальный marker для этогоeventIDбыл успешно стадирован в текущей транзакции и ещё не зафиксирован. Возвращаетfalse, если marker для этогоeventIDуже зафиксирован в хранилище: ничего не стадируется, ошибки нет. Возвращает ошибку, если исход неизвестен; в этом случае нужно остановиться. - SaveAlert(ctx, alert) стадирует ровно один Alert в той же транзакции. Вызывается только после
TryMarkвернулtrue. До успешногоCommitAlert не считается устойчивым. - Commit() атомарно фиксирует стадированный marker и Alert вместе. Только успешный
CommitозначаетOutcomeApplied. Ошибка Commit может быть неоднозначной: транзакция могла быть применена или не применена на стороне хранилища;Rollbackпосле этого является очисткой клиентского состояния и не доказывает, что сервер откатил её. - Rollback() вызывается один раз для любого пути после успешного
Begin, который не завершился подтверждённым успешнымCommit. Это включает ветку дублирования и все ветки ошибок.
Поведение функции
Вы реализуете:
func ProcessOnce(ctx context.Context, store Store, eventID string, alert Alert) (Outcome, error)Ожидаемое поведение по ветвям:
- Первая доставка:
Begin→TryMarkвозвращаетtrue→SaveAlert→Commitвозвращаетnil→ вернутьOutcomeApplied, nil. После подтверждённого успешногоCommitRollbackне вызывается. - Повторная доставка того же eventID:
Begin→TryMarkвозвращаетfalse→Rollback→ вернутьOutcomeAlreadyApplied, nil.SaveAlertиCommitна этой ветке не вызываются. - Ошибка Begin: вернуть
OutcomeUnknownи ту же ошибку. Никакие методыTxнедоступны и не вызываются. - Ошибка TryMark: вызвать
Rollbackодин раз, вернутьOutcomeUnknownи ту же ошибку.SaveAlertиCommitне вызываются. - Ошибка SaveAlert: вызвать
Rollbackодин раз, вернутьOutcomeUnknownи ту же ошибку.Commitне вызывается. - Ошибка Commit: вызвать
Rollbackодин раз как очистку, вернутьOutcomeUnknownи ту же ошибку. Нельзя объявлять пару отсутствующей или устойчивой только на основании ошибки.
Критическое требование к ошибкам: возвращайте точно то значение ошибки, которое вернул store.Begin, tx.TryMark, tx.SaveAlert или tx.Commit — без оборачивания и без замены. Harness внедряет сравнимые sentinel-ошибки и проверяет returnedErr == injectedErr через сравнение значений интерфейса. Любое fmt.Errorf("...: %w", err) сломает проверку.
Два одинаковых сообщения
Fake-store harness проверяет следующий сценарий: два вызова ProcessOnce с одним и тем же eventID и одним и тем же Alert. Первый вызов должен вернуть OutcomeApplied, второй — OutcomeAlreadyApplied. После обоих вызовов в fake-хранилище должен быть ровно один marker и ровно один Alert для этого eventID. Это локальный инвариант fake-транзакций; он не является демонстрацией реального PostgreSQL или гарантией Kafka.
Harness также проверяет порядок вызовов, количество вызовов каждого метода, наличие Rollback на нужных путях и отсутствие SaveAlert и Commit на ветке дублирования и ветках ошибок.
Граница с Kafka
Ваша функция не взаимодействует с Kafka. Внешний consumer подтверждает offset только после того, как ProcessOnce вернул nil-ошибку с исходом OutcomeApplied или OutcomeAlreadyApplied. При любой ошибке consumer останавливается без подтверждения offset. Подтверждение offset — это отдельный вызов, не входящий в локальную транзакцию; этот вызов в данной функции не реализуется.
SQL-вопрос об атомарности
После теории и двух вопросов вы перешли к CODE-задаче; SQL-вопрос об атомарности уже был среди этих вопросов. В нём нужно объяснить, почему marker и Alert должны фиксироваться в одной транзакции, а не двумя отдельными Commit. Для приведённой ниже схемы рассмотрите marker-first и последствия падения между фиксациями. Для alert-first сначала решите, допускает ли показанный внешний ключ фиксацию Alert для нового event_id до marker. Вариант alert-first с возможной повторной вставкой Alert относится только к отдельной гипотетической схеме без этого внешнего ключа и без уникальности source_event_id, а не к DDL ниже. Учебная схема Alerts DB, которую иллюстрирует этот вопрос, выглядит следующим образом:
CREATE TABLE processed_reading_events (
event_id text PRIMARY KEY
);
CREATE TABLE alerts (
source_event_id text PRIMARY KEY
REFERENCES processed_reading_events(event_id),
device_id text NOT NULL,
message text NOT NULL
);Атомарная попытка вставить marker в этой схеме использует конструкцию:
INSERT INTO processed_reading_events(event_id)
VALUES ($1)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;Если строка возвращена — marker новый. Если строка не возвращена (sql.ErrNoRows) — marker уже зафиксирован, Alert не вставляется. Оба INSERT выполняются через один sql.Tx; только успешный tx.Commit делает пару устойчивой. Fake-store проверяет оркестрацию и детерминированное состояние; он не воспроизводит реальные изоляцию, блокировки или устойчивость PostgreSQL — это предмет отдельного SQL-вопроса.
Начальный код
func ProcessOnce(ctx context.Context, store Store, eventID string, alert Alert) (Outcome, error) {
// Implement the supplied transaction contract.
return OutcomeUnknown, nil
}Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Связанные задачи
- Go: Принять только полный ответ о последней статье — задача с проверкой
- HTTP-клиент поиска на Go: обработка ответа и ошибок — задача
- Go: Декодирование артефакта из JSON — задача с проверкой
- Go: Обязательная и необязательная зависимость при отказе — задача с проверкой
- Go: HTTP-обработчик поиска задания через JobStore — задача с проверкой
- Go: HTTP-обработчик расчёта калорийности состава — задача с проверкой
