feat(geo): switch geocoder from self-host Photon to OpenCage API
CI / test (push) Successful in 7m44s
CI / deploy-ift (push) Successful in 3m57s
CI / e2e-ift (push) Successful in 1m16s
CI / deploy-stage (push) Successful in 2m36s
CI / e2e-stage (push) Successful in 4m12s

logic_geo проксирует OpenCage (suggest/geocode/reverse): env OPENCAGE_API_KEY,
OPENCAGE_URL, OPENCAGE_TIMEOUT_MS, OPENCAGE_COUNTRYCODE, OPENCAGE_THROTTLE_RPS
(1 rps под free-тариф, 0 - выключить). HTTP 402/403/429 апстрима ->
503 geo_unavailable; source=opencage. Сервис photon и volume photon-data
удалены из swarm compose; eunit-фикстуры под формат OpenCage.

Refs EventHub/EventHubBack#77
This commit is contained in:
2026-08-18 23:30:15 +03:00
parent 6b70e1336d
commit 73d43e5fdc
5 changed files with 163 additions and 87 deletions
+32 -19
View File
@@ -4,16 +4,22 @@
setup() ->
catch ets:delete(eventhub_geo_cache),
catch ets:delete(eventhub_geo_rl),
catch ets:delete(eventhub_geo_upstream),
logic_geo:ensure(),
application:unset_env(eventhub, geo_http_get),
os:putenv("PHOTON_URL", "http://photon.test"),
os:putenv("OPENCAGE_API_KEY", "test-key"),
os:putenv("OPENCAGE_URL", "http://opencage.test/geocode/v1/json"),
os:putenv("OPENCAGE_THROTTLE_RPS", "0"),
ok.
cleanup(_) ->
application:unset_env(eventhub, geo_http_get),
os:unsetenv("PHOTON_URL"),
os:unsetenv("OPENCAGE_API_KEY"),
os:unsetenv("OPENCAGE_URL"),
os:unsetenv("OPENCAGE_THROTTLE_RPS"),
catch ets:delete(eventhub_geo_cache),
catch ets:delete(eventhub_geo_rl),
catch ets:delete(eventhub_geo_upstream),
ok.
logic_geo_test_() ->
@@ -22,22 +28,21 @@ logic_geo_test_() ->
fun cleanup/1,
[
{"Suggest too short", fun test_suggest_short/0},
{"Suggest parses photon geojson", fun test_suggest_ok/0},
{"Geocode empty features is not_found", fun test_geocode_not_found/0},
{"Unavailable without photon url", fun test_unavailable/0},
{"Suggest parses opencage results", fun test_suggest_ok/0},
{"Geocode empty results is not_found", fun test_geocode_not_found/0},
{"Unavailable without opencage key", fun test_unavailable/0},
{"Upstream quota (402) is unavailable", fun test_quota_exceeded/0},
{"Reverse fallback address", fun test_reverse_empty/0},
{"Format address from properties", fun test_format_address/0}
]}.
sample_geojson() ->
sample_opencage() ->
<<"{
\"features\": [{
\"geometry\": {\"coordinates\": [37.62, 55.75]},
\"properties\": {
\"name\": \"Red Square\",
\"city\": \"Moscow\",
\"country\": \"Russia\"
}
\"status\": {\"code\": 200, \"message\": \"OK\"},
\"results\": [{
\"formatted\": \"Red Square, Moscow, Russia\",
\"geometry\": {\"lat\": 55.75, \"lng\": 37.62},
\"components\": {\"city\": \"Moscow\", \"country\": \"Russia\"}
}]
}">>.
@@ -45,27 +50,35 @@ test_suggest_short() ->
?assertEqual({error, invalid_query}, logic_geo:suggest(<<"ab">>, <<"ru">>, <<"ip">>)).
test_suggest_ok() ->
application:set_env(eventhub, geo_http_get, fun(_) -> {ok, sample_geojson()} end),
application:set_env(eventhub, geo_http_get, fun(_) -> {ok, sample_opencage()} end),
{ok, Hits} = logic_geo:suggest(<<"красная площадь">>, <<"ru">>, <<"ip1">>),
?assertMatch([#{<<"lat">> := 55.75, <<"lon">> := 37.62}], Hits),
[#{<<"address">> := Addr}] = Hits,
?assertNotEqual(<<>>, Addr).
?assertEqual(<<"Red Square, Moscow, Russia">>, Addr).
test_geocode_not_found() ->
application:set_env(eventhub, geo_http_get, fun(_) -> {ok, <<"{\"features\":[]}">>} end),
application:set_env(eventhub, geo_http_get,
fun(_) -> {ok, <<"{\"status\":{\"code\":200},\"results\":[]}">>} end),
?assertEqual({error, not_found},
logic_geo:geocode(<<"nowherexyz">>, <<"ru">>, <<"u1">>)).
test_unavailable() ->
os:unsetenv("PHOTON_URL"),
os:unsetenv("OPENCAGE_API_KEY"),
application:unset_env(eventhub, geo_http_get),
?assertEqual({error, unavailable},
logic_geo:suggest(<<"москва центр">>, <<"ru">>, <<"ip2">>)).
test_quota_exceeded() ->
application:set_env(eventhub, geo_http_get,
fun(_) -> {error, {http_status, 402}} end),
?assertEqual({error, unavailable},
logic_geo:suggest(<<"москва центр">>, <<"ru">>, <<"ip3">>)).
test_reverse_empty() ->
application:set_env(eventhub, geo_http_get, fun(_) -> {ok, <<"{\"features\":[]}">>} end),
application:set_env(eventhub, geo_http_get,
fun(_) -> {ok, <<"{\"status\":{\"code\":200},\"results\":[]}">>} end),
{ok, Hit} = logic_geo:reverse(55.75, 37.62, <<"ru">>, <<"u2">>),
?assertEqual(<<"photon">>, maps:get(<<"source">>, Hit)),
?assertEqual(<<"opencage">>, maps:get(<<"source">>, Hit)),
?assertEqual(55.75, maps:get(<<"lat">>, Hit)).
test_format_address() ->