diff --git a/docker/docker-compose.swarm.yml b/docker/docker-compose.swarm.yml index f177c06..a86d0e8 100644 --- a/docker/docker-compose.swarm.yml +++ b/docker/docker-compose.swarm.yml @@ -76,12 +76,17 @@ services: - eventhub - eventhub-node volumes: + # Per-task volume when scaling replicas > 1 (never share one Mnesia dir). - type: volume - source: eventhub-data + source: eventhub-data-{{.Task.Slot}} target: /app/data deploy: replicas: 1 endpoint_mode: dnsrr + update_config: + parallelism: 1 + delay: 30s + order: stop-first restart_policy: condition: any labels: @@ -222,8 +227,8 @@ networks: driver: overlay volumes: - eventhub-data: -# name: 'eventhub-data-{{.Task.Slot}}' + # eventhub-data-{{.Task.Slot}} created by Swarm from service mount template + # (do not use a single shared volume for multi-replica Mnesia) prometheus-data: grafana-data: traefik-logs: diff --git a/src/core/core_verification.erl b/src/core/core_verification.erl index ef3ff53..2745903 100644 --- a/src/core/core_verification.erl +++ b/src/core/core_verification.erl @@ -34,17 +34,19 @@ verify_token(Token) -> %%%------------------------------------------------------------------- %%% @doc Возвращает существующий токен пользователя или создаёт новый. +%%% Допускает несколько токенов на user_id (register + повторные вызовы). %%% @end %%%------------------------------------------------------------------- -spec get_or_create_token(UserId :: binary()) -> {ok, Token :: binary(), ExpiresAt :: calendar:datetime()} | {error, not_found}. get_or_create_token(UserId) -> - case mnesia:dirty_match_object(#verification{user_id = UserId, _ = '_'}) of - [V] -> {ok, V#verification.token, V#verification.expires_at}; - [] -> - case core_user:get_by_id(UserId) of - {ok, _} -> create_token(UserId); - Error -> Error + case find_token(UserId) of + {ok, Token, ExpiresAt} -> + {ok, Token, ExpiresAt}; + not_found -> + case user_exists(UserId) of + true -> create_token(UserId); + false -> {error, not_found} end end. @@ -55,4 +57,24 @@ get_or_create_token(UserId) -> -spec delete_token(Token :: binary()) -> ok. delete_token(Token) -> mnesia:dirty_delete(verification, Token), - ok. \ No newline at end of file + ok. + +%%-------------------------------------------------------------------- +find_token(UserId) -> + case mnesia:dirty_match_object(#verification{user_id = UserId, _ = '_'}) of + [#verification{token = Token, expires_at = ExpiresAt} | _] -> + {ok, Token, ExpiresAt}; + [] -> + not_found + end. + +%% dirty_read first; transactional read as fallback for cluster replica lag. +user_exists(UserId) -> + case core_user:get_by_id(UserId) of + {ok, _} -> true; + {error, not_found} -> + case mnesia:transaction(fun() -> mnesia:read(user, UserId) end) of + {atomic, [_ | _]} -> true; + _ -> false + end + end. diff --git a/src/infra/infra_mnesia.erl b/src/infra/infra_mnesia.erl index 88ebbf2..d32eb98 100644 --- a/src/infra/infra_mnesia.erl +++ b/src/infra/infra_mnesia.erl @@ -229,7 +229,7 @@ add_local_ram_copy(Tab) -> end. wait_for_tables_available() -> - lists:foreach(fun(Tab) -> wait_for_table(Tab) end, ?DISC_TABLES). + lists:foreach(fun(Tab) -> wait_for_table(Tab) end, ?DISC_TABLES ++ ?RAM_TABLES). wait_for_table(Tab) -> case lists:member(Tab, mnesia:system_info(tables)) of @@ -260,6 +260,12 @@ prune_dead_nodes() -> false -> ok end end, ?DISC_TABLES), + lists:foreach(fun(Tab) -> + case lists:member(Node, mnesia:table_info(Tab, ram_copies)) of + true -> catch mnesia:del_table_copy(Tab, Node); + false -> ok + end + end, ?RAM_TABLES), catch mnesia:del_table_copy(schema, Node) end, DeadNodes). @@ -327,6 +333,7 @@ create_indices() -> mnesia:add_table_index(calendar_specialist, user_id), mnesia:add_table_index(user, nickname), mnesia:add_table_index(user, email), + mnesia:add_table_index(verification, user_id), mnesia:add_table_index(notification, user_id), mnesia:add_table_index(notification, is_read), mnesia:add_table_index(auth_session, family_id), diff --git a/test/api/api_test_runner.erl b/test/api/api_test_runner.erl index e134ecd..6ed5425 100644 --- a/test/api/api_test_runner.erl +++ b/test/api/api_test_runner.erl @@ -402,13 +402,25 @@ create_event(Token, CalId, Params) -> %% @doc Подтверждает email пользователя, используя админский эндпоинт %% для получения верификационного токена и публичный /v1/verify. +%% Retries briefly: IFT Traefik may hit another replica before Mnesia settles. -spec verify_user(AdminToken :: binary(), UserId :: binary()) -> ok. verify_user(AdminToken, UserId) -> - #{<<"token">> := Token} = admin_get( - <<"/v1/admin/users/", UserId/binary, "/verification-token">>, AdminToken), - {ok, 200, _, _} = client_request(post, <<"/v1/verify">>, <<>>, - jsx:encode(#{<<"token">> => Token})), - ok. + verify_user(AdminToken, UserId, 8). + +verify_user(AdminToken, UserId, RetriesLeft) -> + Path = <<"/v1/admin/users/", UserId/binary, "/verification-token">>, + case admin_request(get, Path, AdminToken) of + {ok, 200, _, Body} -> + #{<<"token">> := Token} = jsx:decode(list_to_binary(Body), [return_maps]), + {ok, 200, _, _} = client_request(post, <<"/v1/verify">>, <<>>, + jsx:encode(#{<<"token">> => Token})), + ok; + {ok, 404, _, _} when RetriesLeft > 1 -> + timer:sleep(100), + verify_user(AdminToken, UserId, RetriesLeft - 1); + Other -> + error({verify_user_failed, UserId, Other}) + end. %%%=================================================================== %%% Внутренние функции