geopulse — бекенд GPS fleet-tracking
Огляд
Незалежний demo-проєкт: REST API на Go для GPS fleet-tracking — техніка, кругові геозони, прийом позицій та детекція enter/exit подій на PostGIS. Моделює патерн, що стоїть за telematics-інтеграціями автопарків: трекер (або платформа на кшталт Traccar, що ретранслює від нього) шле позиції пінгами, і бекенд має перетворити цей сирий потік на достовірні події “техніка увійшла/вийшла із зони X”.
Результат: точні, без дублів geofence-події з потоку позицій, що дублюється й приходить не по порядку — реальний, а не гіпотетичний режим відмови GPS-трекерів — доведено проти прямого стану бази даних, не просто заявлено.
Справжня складна задача
Наївна реакція на кожен пінг — “чи ми зараз всередині геозони X? запалити подію” — дає два реальні баги: ретрансльований пінг повторно запалює “enter” для техніки, яка вже годину припаркована всередині зони, а пінг не по порядку (затриманий мережевим ретраєм) може змусити техніку, що вже покинула зону, здаватись такою, що знову в неї увійшла — бо координати застарілого пінга так кажуть. Обидва — задокументовані, реальні режими відмов telematics-систем — другий саме те “хибне geofence-спрацювання через затриману геодату”, описане в кейсі ivr-elevenlabs, з іншого боку того самого класу багу.
Архітектура

Кожен пінг проходить одну транзакцію: advisory-lock техніки → вставка-або-ігнор позиції → відхилення, якщо не по порядку → перевірка containment ST_DWithin для кожної геозони → чиста функція рішення → коміт того, що вийшло.
Ключові інженерні рішення
| Рішення | Чому |
|---|---|
positions.UNIQUE(vehicle_id, recorded_at) + ON CONFLICT DO NOTHING |
Точні ретрансляції поглинаються на рівні БД — жодного дубльованого рядка, жодного повторного оцінювання |
| Перевірка “не по порядку” всередині тієї самої транзакції, що й вставка | Пінг старіший за поточну останню позицію техніки зберігається в історії, але ніколи не рухає стан геозони назад у часі |
pg_advisory_xact_lock(vehicle_id), утримуваний на всю транзакцію прийому-й-оцінки, не лише вставку |
Два одночасні пінги для тієї самої техніки не можуть обидва прочитати той самий “поточний стан” і незалежно вирішити запалити перехід — пінги для різної техніки все ще йдуть повністю паралельно, бо ключ блокування — per-vehicle |
NextEventType винесена як чиста функція “зараз всередині?” + “який був останній тип події?” |
Жодного I/O, повністю юніт-тестована — включно з тестом послідовності, що симулює техніку на межі геозони через кілька пінгів, точний сценарій, що спричиняє флепінг enter/exit без відстеження останнього відомого стану |
| Кругові геозони (центр + радіус), не полігони | Покриває типовий реальний випадок — депо, радіус доставки — простішим PostGIS-запитом (ST_DWithin), ніж containment полігонів, не жертвуючи справжньою складною частиною (гарантіями конкурентності/порядку) |
Результати
- Повний REST API: JWT-авторизація, CRUD техніки/геозон, ендпоінт прийому позицій, захищений
X-Webhook-Secret(machine-to-machine, не дія диспетчера) - Рішення enter/exit чисте й повністю юніт-тестоване — включно з тестом на відсутність флепінгу — без потреби в БД для
go test ./... - Перевірено наскрізно через
docker compose up(образpostgis/postgis): міграції застосовуються, повна послідовність enter → все ще всередині → exit дає рівно 2 події (не одну на пінг), точний дубль-пінг поглинається без нових подій, пінг не по порядку зберігається, але інертний - Гарантія конкурентності підтверджена проти прямого стану БД: сплеск конкурентних/швидких пінгів у вже “всередині”-стан техніки дав нуль зайвих подій — 5 рядків
geofence_eventsза всю тестову сесію (enter/exit/enter/exit/enter), точно відповідаючи реальній історії in/out техніки, попри значно більше 5 надісланих пінгів позицій
Код: github.com/valpere/geopulse
Стек
Go · chi (router) · pgx/v5 (без ORM) · golang-jwt · bcrypt · PostgreSQL + PostGIS (geography, ST_DWithin, pg_advisory_xact_lock) · Docker Compose · testify