Ferrite.Data/UpdateState/RedisUpdatesContext.cs
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2022-2026 Aykut Alparslan KOC
using StackExchange.Redis;
using Ferrite.Data.Counters;
using Ferrite.Data.MessageBoxes;
namespace Ferrite.Data.UpdateState;
public class RedisUpdatesContext : IUpdatesContext
{
private readonly ConnectionMultiplexer _redis;
private readonly long? _authKeyId;
private readonly long _userId;
private readonly IAtomicCounter _counter;
private readonly IMessageBox _commonMessageBox;
private readonly ISecretMessageBox? _secondaryMessageBox;
private static readonly TimeSpan PublicationWait = TimeSpan.FromSeconds(2);
private const string AdvanceDeliveredScript =
"local current = tonumber(redis.call('GET', KEYS[1]) or '0') " +
"if tonumber(ARGV[1]) > current then " +
"redis.call('SET', KEYS[1], ARGV[1]) end " +
"return redis.call('GET', KEYS[1])";
private const string SettlePtsScript = """
local committed = tonumber(redis.call('GET', KEYS[2]) or '1')
local first, last = tonumber(ARGV[1]), tonumber(ARGV[2])
if last <= committed then return 0 end
local previous = redis.call('ZSCORE', KEYS[1], ARGV[2])
if not previous or first < tonumber(previous) then
redis.call('ZADD', KEYS[1], first, ARGV[2])
end
return 1
""";
private const string ExtendCommittedPtsScript = """
local committed = math.max(tonumber(ARGV[1]), tonumber(redis.call('GET', KEYS[2]) or '1'))
local ranges = redis.call('ZRANGE', KEYS[1], 0, -1, 'WITHSCORES')
for i = 1, #ranges, 2 do
if tonumber(ranges[i + 1]) > committed + 1 then break end
committed = math.max(committed, tonumber(ranges[i]))
redis.call('ZREM', KEYS[1], ranges[i])
end
redis.call('SET', KEYS[2], committed)
return committed
""";
public RedisUpdatesContext(ConnectionMultiplexer redis, long? authKeyId, long userId)
{
_redis = redis;
_authKeyId = authKeyId;
_userId = userId;
_counter = new RedisCounter(redis,
authKeyId != null ? $"seq:updates:auth:{authKeyId}" : $"seq:updates:{userId}");
_commonMessageBox = new RedisMessageBox(redis, userId);
_secondaryMessageBox = authKeyId != null ? new RedisSecretMessageBox(redis, (long)authKeyId) : null;
}
public async ValueTask<int> Pts()
{
return await _commonMessageBox.Pts();
}
public async ValueTask<int> IncrementPtsForMessage(int peerType, long peerId, int messageId)
{
return await _commonMessageBox.IncrementPtsForMessage(peerType,peerId, messageId);
}
public async ValueTask<int> NextMessageId()
{
return await _commonMessageBox.NextMessageId();
}
public async ValueTask<int> ReadMessages(int peerType, long peerId, int maxId)
{
return await _commonMessageBox.ReadMessages(peerType, peerId, maxId);
}
public async ValueTask<int> ReadMessagesMaxId(int peerType, long peerId)
{
return await _commonMessageBox.ReadMessagesMaxId(peerType, peerId);
}
public async ValueTask<int> UnreadMessages()
{
return await _commonMessageBox.UnreadMessages();
}
public async ValueTask<int> UnreadMessages(int peerType, long peerId)
{
return await _commonMessageBox.UnreadMessages(peerType, peerId);
}
public async ValueTask<int> IncrementPts()
{
int pts = await _commonMessageBox.IncrementPts();
await RecordSessionDelivery(pts);
return pts;
}
public async ValueTask<int> IncrementPts(int count)
{
int pts = await _commonMessageBox.IncrementPts(count);
await RecordSessionDelivery(pts);
return pts;
}
private ValueTask RecordSessionDelivery(int pts) =>
_authKeyId != null ? AdvanceDeliveredPts(pts) : ValueTask.CompletedTask;
public async ValueTask<int> Qts()
{
return _secondaryMessageBox != null ? await _secondaryMessageBox.Qts() : 0;
}
public async ValueTask<int> IncrementQts()
{
return _secondaryMessageBox != null ? await _secondaryMessageBox.IncrementQts() : 0;
}
public async Task<int> Seq()
{
return (int)await _counter.Get();
}
public async Task<int> IncrementSeq()
{
return (int)await _counter.IncrementAndGet();
}
public async ValueTask BeginPtsPublication()
{
IDatabase db = _redis.GetDatabase();
RedisKey key = $"updates:pending-publish:{_userId}";
await db.StringIncrementAsync(key);
await db.KeyExpireAsync(key, TimeSpan.FromSeconds(30));
}
public async ValueTask CompletePtsPublication()
{
IDatabase db = _redis.GetDatabase();
RedisKey key = $"updates:pending-publish:{_userId}";
long remaining = await db.StringDecrementAsync(key);
if (remaining <= 0)
{
await db.KeyDeleteAsync(key);
}
}
public async ValueTask WaitForPtsPublications()
{
IDatabase db = _redis.GetDatabase();
RedisKey key = $"updates:pending-publish:{_userId}";
DateTime deadline = DateTime.UtcNow + PublicationWait;
while ((long)await db.StringGetAsync(key) > 0)
{
if (DateTime.UtcNow >= deadline) return;
await Task.Delay(5);
}
}
public async ValueTask<int> DeliveredPts()
{
RedisValue value = await _redis.GetDatabase().StringGetAsync(
$"updates:delivered-pts:{_userId}");
return value.HasValue ? (int)value : 0;
}
public async ValueTask AdvanceDeliveredPts(int pts)
{
await _redis.GetDatabase().ScriptEvaluateAsync(AdvanceDeliveredScript,
new RedisKey[] { $"updates:delivered-pts:{_userId}" },
new RedisValue[] { pts });
}
public async ValueTask<int> PendingPtsPublications()
{
RedisValue value = await _redis.GetDatabase().StringGetAsync(
$"updates:pending-publish:{_userId}");
return value.HasValue ? (int)(long)value : 0;
}
public async ValueTask SettlePts(int first, int last)
{
ArgumentOutOfRangeException.ThrowIfLessThan(first, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(last, first);
await _redis.GetDatabase().ScriptEvaluateAsync(SettlePtsScript,
new RedisKey[] { $"updates:settled-pts:{_userId}", $"updates:committed-pts:{_userId}" },
new RedisValue[] { first, last });
}
public async ValueTask<int> ExtendCommittedPts(int committed)
{
return (int)await _redis.GetDatabase().ScriptEvaluateAsync(ExtendCommittedPtsScript,
new RedisKey[] { $"updates:settled-pts:{_userId}", $"updates:committed-pts:{_userId}" },
new RedisValue[] { committed });
}
}