fix(mnesia): per-slot volumes hint and harden verification lookup.
Avoid shared Mnesia dir across replicas; make get_or_create_token and CT verify_user resilient to brief cross-node lag. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -76,12 +76,17 @@ services:
|
|||||||
- eventhub
|
- eventhub
|
||||||
- eventhub-node
|
- eventhub-node
|
||||||
volumes:
|
volumes:
|
||||||
|
# Per-task volume when scaling replicas > 1 (never share one Mnesia dir).
|
||||||
- type: volume
|
- type: volume
|
||||||
source: eventhub-data
|
source: eventhub-data-{{.Task.Slot}}
|
||||||
target: /app/data
|
target: /app/data
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
endpoint_mode: dnsrr
|
endpoint_mode: dnsrr
|
||||||
|
update_config:
|
||||||
|
parallelism: 1
|
||||||
|
delay: 30s
|
||||||
|
order: stop-first
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
labels:
|
labels:
|
||||||
@@ -222,8 +227,8 @@ networks:
|
|||||||
driver: overlay
|
driver: overlay
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
eventhub-data:
|
# eventhub-data-{{.Task.Slot}} created by Swarm from service mount template
|
||||||
# name: 'eventhub-data-{{.Task.Slot}}'
|
# (do not use a single shared volume for multi-replica Mnesia)
|
||||||
prometheus-data:
|
prometheus-data:
|
||||||
grafana-data:
|
grafana-data:
|
||||||
traefik-logs:
|
traefik-logs:
|
||||||
|
|||||||
@@ -34,17 +34,19 @@ verify_token(Token) ->
|
|||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @doc Возвращает существующий токен пользователя или создаёт новый.
|
%%% @doc Возвращает существующий токен пользователя или создаёт новый.
|
||||||
|
%%% Допускает несколько токенов на user_id (register + повторные вызовы).
|
||||||
%%% @end
|
%%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-spec get_or_create_token(UserId :: binary()) ->
|
-spec get_or_create_token(UserId :: binary()) ->
|
||||||
{ok, Token :: binary(), ExpiresAt :: calendar:datetime()} | {error, not_found}.
|
{ok, Token :: binary(), ExpiresAt :: calendar:datetime()} | {error, not_found}.
|
||||||
get_or_create_token(UserId) ->
|
get_or_create_token(UserId) ->
|
||||||
case mnesia:dirty_match_object(#verification{user_id = UserId, _ = '_'}) of
|
case find_token(UserId) of
|
||||||
[V] -> {ok, V#verification.token, V#verification.expires_at};
|
{ok, Token, ExpiresAt} ->
|
||||||
[] ->
|
{ok, Token, ExpiresAt};
|
||||||
case core_user:get_by_id(UserId) of
|
not_found ->
|
||||||
{ok, _} -> create_token(UserId);
|
case user_exists(UserId) of
|
||||||
Error -> Error
|
true -> create_token(UserId);
|
||||||
|
false -> {error, not_found}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@@ -55,4 +57,24 @@ get_or_create_token(UserId) ->
|
|||||||
-spec delete_token(Token :: binary()) -> ok.
|
-spec delete_token(Token :: binary()) -> ok.
|
||||||
delete_token(Token) ->
|
delete_token(Token) ->
|
||||||
mnesia:dirty_delete(verification, Token),
|
mnesia:dirty_delete(verification, Token),
|
||||||
ok.
|
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.
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ add_local_ram_copy(Tab) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
wait_for_tables_available() ->
|
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) ->
|
wait_for_table(Tab) ->
|
||||||
case lists:member(Tab, mnesia:system_info(tables)) of
|
case lists:member(Tab, mnesia:system_info(tables)) of
|
||||||
@@ -260,6 +260,12 @@ prune_dead_nodes() ->
|
|||||||
false -> ok
|
false -> ok
|
||||||
end
|
end
|
||||||
end, ?DISC_TABLES),
|
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)
|
catch mnesia:del_table_copy(schema, Node)
|
||||||
end, DeadNodes).
|
end, DeadNodes).
|
||||||
|
|
||||||
@@ -327,6 +333,7 @@ create_indices() ->
|
|||||||
mnesia:add_table_index(calendar_specialist, user_id),
|
mnesia:add_table_index(calendar_specialist, user_id),
|
||||||
mnesia:add_table_index(user, nickname),
|
mnesia:add_table_index(user, nickname),
|
||||||
mnesia:add_table_index(user, email),
|
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, user_id),
|
||||||
mnesia:add_table_index(notification, is_read),
|
mnesia:add_table_index(notification, is_read),
|
||||||
mnesia:add_table_index(auth_session, family_id),
|
mnesia:add_table_index(auth_session, family_id),
|
||||||
|
|||||||
@@ -402,13 +402,25 @@ create_event(Token, CalId, Params) ->
|
|||||||
|
|
||||||
%% @doc Подтверждает email пользователя, используя админский эндпоинт
|
%% @doc Подтверждает email пользователя, используя админский эндпоинт
|
||||||
%% для получения верификационного токена и публичный /v1/verify.
|
%% для получения верификационного токена и публичный /v1/verify.
|
||||||
|
%% Retries briefly: IFT Traefik may hit another replica before Mnesia settles.
|
||||||
-spec verify_user(AdminToken :: binary(), UserId :: binary()) -> ok.
|
-spec verify_user(AdminToken :: binary(), UserId :: binary()) -> ok.
|
||||||
verify_user(AdminToken, UserId) ->
|
verify_user(AdminToken, UserId) ->
|
||||||
#{<<"token">> := Token} = admin_get(
|
verify_user(AdminToken, UserId, 8).
|
||||||
<<"/v1/admin/users/", UserId/binary, "/verification-token">>, AdminToken),
|
|
||||||
{ok, 200, _, _} = client_request(post, <<"/v1/verify">>, <<>>,
|
verify_user(AdminToken, UserId, RetriesLeft) ->
|
||||||
jsx:encode(#{<<"token">> => Token})),
|
Path = <<"/v1/admin/users/", UserId/binary, "/verification-token">>,
|
||||||
ok.
|
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.
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% Внутренние функции
|
%%% Внутренние функции
|
||||||
|
|||||||
Reference in New Issue
Block a user