fluxer/fluxer_gateway/src/push/push_cache.erl
2026-01-01 21:05:54 +00:00

162 lines
5.6 KiB
Erlang

%% Copyright (C) 2026 Fluxer Contributors
%%
%% This file is part of Fluxer.
%%
%% Fluxer is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%%
%% Fluxer is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
-module(push_cache).
-export([update_lru/2]).
-export([get_user_push_subscriptions/2]).
-export([cache_user_subscriptions/3]).
-export([get_user_badge_count/2]).
-export([cache_user_badge_count/4]).
-export([estimate_subscriptions_size/1]).
-export([evict_if_needed/4]).
-export([invalidate_user_badge_count/2]).
update_lru(Key, Lru) ->
NewLru = lists:delete(Key, Lru),
[Key | NewLru].
get_user_push_subscriptions(UserId, State) ->
Key = {subscriptions, UserId},
PushSubscriptionsCache = maps:get(push_subscriptions_cache, State, #{}),
case maps:get(Key, PushSubscriptionsCache, undefined) of
undefined ->
[];
Subs ->
Subs
end.
cache_user_subscriptions(UserId, Subscriptions, State) ->
Key = {subscriptions, UserId},
NewSubsSize = estimate_subscriptions_size(Subscriptions),
OldSubsSize =
case maps:get(Key, maps:get(push_subscriptions_cache, State, #{}), undefined) of
undefined -> 0;
OldSubs -> estimate_subscriptions_size(OldSubs)
end,
SizeDelta = NewSubsSize - OldSubsSize,
PushSubscriptionsLru = maps:get(push_subscriptions_lru, State, []),
NewLru = update_lru(Key, PushSubscriptionsLru),
PushSubscriptionsCache = maps:get(push_subscriptions_cache, State, #{}),
NewCache = maps:put(Key, Subscriptions, PushSubscriptionsCache),
PushSubscriptionsSize = maps:get(push_subscriptions_size, State, 0),
NewSize = PushSubscriptionsSize + SizeDelta,
MaxBytes =
case maps:get(push_subscriptions_max_mb, State, undefined) of
undefined -> NewSize;
Mb -> Mb * 1024 * 1024
end,
{FinalCache, FinalLru, FinalSize} = evict_if_needed(
NewCache, NewLru, NewSize, MaxBytes
),
State#{
push_subscriptions_cache => FinalCache,
push_subscriptions_lru => FinalLru,
push_subscriptions_size => FinalSize
}.
get_user_badge_count(UserId, State) ->
Key = {badge_count, UserId},
BadgeCountsCache = maps:get(badge_counts_cache, State, #{}),
case maps:get(Key, BadgeCountsCache, undefined) of
undefined ->
undefined;
Badge ->
Badge
end.
cache_user_badge_count(UserId, BadgeCount, CachedAt, State) ->
Key = {badge_count, UserId},
NewBadge = {BadgeCount, CachedAt},
OldBadgeSize =
case maps:get(Key, maps:get(badge_counts_cache, State, #{}), undefined) of
undefined -> 0;
OldBadge -> estimate_badge_count_size(OldBadge)
end,
NewBadgeSize = estimate_badge_count_size(NewBadge),
SizeDelta = NewBadgeSize - OldBadgeSize,
BadgeCountsLru = maps:get(badge_counts_lru, State, []),
NewLru = update_lru(Key, BadgeCountsLru),
BadgeCountsCache = maps:get(badge_counts_cache, State, #{}),
NewCache = maps:put(Key, NewBadge, BadgeCountsCache),
BadgeCountsSize = maps:get(badge_counts_size, State, 0),
NewSize = BadgeCountsSize + SizeDelta,
MaxBytes =
case maps:get(badge_counts_max_mb, State, undefined) of
undefined -> NewSize;
Mb -> Mb * 1024 * 1024
end,
{FinalCache, FinalLru, FinalSize} = evict_if_needed(
NewCache, NewLru, NewSize, MaxBytes
),
State#{
badge_counts_cache => FinalCache,
badge_counts_lru => FinalLru,
badge_counts_size => FinalSize
}.
estimate_subscriptions_size(Subscriptions) ->
length(Subscriptions) * 200.
estimate_badge_count_size({_Count, _Timestamp}) ->
64.
evict_if_needed(Cache, Lru, Size, MaxBytes) when Size > MaxBytes ->
evict_oldest(Cache, Lru, Size, MaxBytes, lists:reverse(Lru));
evict_if_needed(Cache, Lru, Size, _MaxBytes) ->
{Cache, Lru, Size}.
evict_oldest(Cache, Lru, Size, _MaxBytes, []) ->
{Cache, Lru, Size};
evict_oldest(Cache, Lru, Size, MaxBytes, [OldestKey | Remaining]) ->
case maps:get(OldestKey, Cache, undefined) of
undefined ->
evict_oldest(Cache, Lru, Size, MaxBytes, Remaining);
OldSubs ->
NewCache = maps:remove(OldestKey, Cache),
NewSize = Size - estimate_subscriptions_size(OldSubs),
NewLru = lists:delete(OldestKey, Lru),
evict_if_needed(NewCache, NewLru, NewSize, MaxBytes)
end.
invalidate_user_badge_count(UserId, State) ->
Key = {badge_count, UserId},
BadgeCountsCache = maps:get(badge_counts_cache, State, #{}),
case maps:get(Key, BadgeCountsCache, undefined) of
undefined ->
State;
Badge ->
NewCache = maps:remove(Key, BadgeCountsCache),
BadgeCountsLru = lists:delete(Key, maps:get(badge_counts_lru, State, [])),
BadgeCountsSize = maps:get(badge_counts_size, State, 0),
NewSize = max(0, BadgeCountsSize - estimate_badge_count_size(Badge)),
State#{
badge_counts_cache => NewCache,
badge_counts_lru => BadgeCountsLru,
badge_counts_size => NewSize
}
end.