feat(batch19): implement service reply and stream import/export methods
This commit is contained in:
@@ -2768,6 +2768,420 @@ public sealed class Account : INatsAccount
|
||||
ServiceImportReply = [.. prefix, (byte)'.'];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new service reply subject.
|
||||
/// Mirrors Go <c>(a *Account) newServiceReply(tracking bool) []byte</c>.
|
||||
/// </summary>
|
||||
internal byte[] NewServiceReply(bool tracking)
|
||||
{
|
||||
bool createdPrefix = false;
|
||||
byte[] replyPrefix;
|
||||
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (ServiceImportReply == null)
|
||||
{
|
||||
CreateRespWildcard();
|
||||
createdPrefix = true;
|
||||
}
|
||||
|
||||
replyPrefix = ServiceImportReply ?? Encoding.ASCII.GetBytes("_R_.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
|
||||
if (createdPrefix)
|
||||
_ = SubscribeServiceImportResponse(Encoding.ASCII.GetString([.. replyPrefix, (byte)'>']));
|
||||
|
||||
const string alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
Span<byte> randomPart = stackalloc byte[20];
|
||||
ulong random = (ulong)Random.Shared.NextInt64();
|
||||
for (var i = 0; i < randomPart.Length; i++)
|
||||
{
|
||||
randomPart[i] = (byte)alphabet[(int)(random % (ulong)alphabet.Length)];
|
||||
random /= (ulong)alphabet.Length;
|
||||
}
|
||||
|
||||
var reply = new List<byte>(replyPrefix.Length + randomPart.Length + 2);
|
||||
reply.AddRange(replyPrefix);
|
||||
reply.AddRange(randomPart.ToArray());
|
||||
|
||||
if (tracking)
|
||||
{
|
||||
reply.Add((byte)'.');
|
||||
reply.Add((byte)'T');
|
||||
}
|
||||
|
||||
return [.. reply];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the response threshold for an exported service.
|
||||
/// Mirrors Go <c>(a *Account) ServiceExportResponseThreshold(...)</c>.
|
||||
/// </summary>
|
||||
public (TimeSpan Threshold, Exception? Error) ServiceExportResponseThreshold(string export)
|
||||
{
|
||||
_mu.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var serviceExport = GetServiceExport(export);
|
||||
if (serviceExport == null)
|
||||
return (TimeSpan.Zero, new InvalidOperationException($"no export defined for \"{export}\""));
|
||||
return (serviceExport.ResponseThreshold, null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets max response delivery time for an exported service.
|
||||
/// Mirrors Go <c>(a *Account) SetServiceExportResponseThreshold(...)</c>.
|
||||
/// </summary>
|
||||
public Exception? SetServiceExportResponseThreshold(string export, TimeSpan maxTime)
|
||||
{
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (IsClaimAccount())
|
||||
return new InvalidOperationException("claim based accounts can not be updated directly");
|
||||
|
||||
var serviceExport = GetServiceExport(export);
|
||||
if (serviceExport == null)
|
||||
return new InvalidOperationException($"no export defined for \"{export}\"");
|
||||
|
||||
serviceExport.ResponseThreshold = maxTime;
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables/disables cross-account trace propagation on a service export.
|
||||
/// Mirrors Go <c>(a *Account) SetServiceExportAllowTrace(...)</c>.
|
||||
/// </summary>
|
||||
public Exception? SetServiceExportAllowTrace(string export, bool allowTrace)
|
||||
{
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var serviceExport = GetServiceExport(export);
|
||||
if (serviceExport == null)
|
||||
return new InvalidOperationException($"no export defined for \"{export}\"");
|
||||
|
||||
serviceExport.AllowTrace = allowTrace;
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates internal response service import entry.
|
||||
/// Mirrors Go <c>(a *Account) addRespServiceImport(...)</c>.
|
||||
/// </summary>
|
||||
internal ServiceImportEntry AddRespServiceImport(Account destination, string to, ServiceImportEntry originalServiceImport, bool tracking, Dictionary<string, string[]>? header)
|
||||
{
|
||||
var newReply = Encoding.ASCII.GetString(originalServiceImport.Account?.NewServiceReply(tracking) ?? NewServiceReply(tracking));
|
||||
|
||||
ServiceImportEntry responseImport;
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
responseImport = new ServiceImportEntry
|
||||
{
|
||||
Account = destination,
|
||||
ServiceExport = originalServiceImport.ServiceExport,
|
||||
From = newReply,
|
||||
To = to,
|
||||
ResponseType = originalServiceImport.ResponseType,
|
||||
IsResponse = true,
|
||||
Share = originalServiceImport.Share,
|
||||
Timestamp = UtcNowUnixNanos(),
|
||||
Tracking = tracking && originalServiceImport.ResponseType == ServiceRespType.Singleton,
|
||||
TrackingHeader = header,
|
||||
Latency = tracking && originalServiceImport.ResponseType == ServiceRespType.Singleton
|
||||
? originalServiceImport.Latency
|
||||
: null,
|
||||
};
|
||||
|
||||
Exports.Responses ??= new Dictionary<string, ServiceImportEntry>(StringComparer.Ordinal);
|
||||
Exports.Responses[newReply] = responseImport;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
|
||||
destination.AddReverseRespMapEntry(this, to, newReply);
|
||||
return responseImport;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds stream import with optional claim context.
|
||||
/// Mirrors Go <c>(a *Account) AddStreamImportWithClaim(...)</c>.
|
||||
/// </summary>
|
||||
public Exception? AddStreamImportWithClaim(Account account, string from, string prefix, object? importClaim) =>
|
||||
AddStreamImportWithClaimInternal(account, from, prefix, false, importClaim);
|
||||
|
||||
/// <summary>
|
||||
/// Internal stream import add helper.
|
||||
/// Mirrors Go <c>(a *Account) addStreamImportWithClaim(...)</c>.
|
||||
/// </summary>
|
||||
internal Exception? AddStreamImportWithClaimInternal(Account account, string from, string prefix, bool allowTrace, object? importClaim)
|
||||
{
|
||||
if (account == null)
|
||||
return ServerErrors.ErrMissingAccount;
|
||||
if (!account.CheckStreamImportAuthorized(this, from, importClaim))
|
||||
return ServerErrors.ErrStreamImportAuthorization;
|
||||
|
||||
if (!string.IsNullOrEmpty(prefix))
|
||||
{
|
||||
if (SubscriptionIndex.SubjectHasWildcard(prefix))
|
||||
return ServerErrors.ErrStreamImportBadPrefix;
|
||||
if (!prefix.EndsWith(".", StringComparison.Ordinal))
|
||||
prefix += '.';
|
||||
}
|
||||
|
||||
return AddMappedStreamImportWithClaimInternal(account, from, prefix + from, allowTrace, importClaim);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience helper for mapped stream imports without claim.
|
||||
/// Mirrors Go <c>(a *Account) AddMappedStreamImport(...)</c>.
|
||||
/// </summary>
|
||||
public Exception? AddMappedStreamImport(Account account, string from, string to) =>
|
||||
AddMappedStreamImportWithClaim(account, from, to, null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds mapped stream import with optional claim.
|
||||
/// Mirrors Go <c>(a *Account) AddMappedStreamImportWithClaim(...)</c>.
|
||||
/// </summary>
|
||||
public Exception? AddMappedStreamImportWithClaim(Account account, string from, string to, object? importClaim) =>
|
||||
AddMappedStreamImportWithClaimInternal(account, from, to, false, importClaim);
|
||||
|
||||
/// <summary>
|
||||
/// Internal mapped stream import add helper.
|
||||
/// Mirrors Go <c>(a *Account) addMappedStreamImportWithClaim(...)</c>.
|
||||
/// </summary>
|
||||
internal Exception? AddMappedStreamImportWithClaimInternal(Account account, string from, string to, bool allowTrace, object? importClaim)
|
||||
{
|
||||
if (account == null)
|
||||
return ServerErrors.ErrMissingAccount;
|
||||
if (!account.CheckStreamImportAuthorized(this, from, importClaim))
|
||||
return ServerErrors.ErrStreamImportAuthorization;
|
||||
|
||||
if (string.IsNullOrEmpty(to))
|
||||
to = from;
|
||||
|
||||
var cycleErr = StreamImportFormsCycle(account, to) ?? StreamImportFormsCycle(account, from);
|
||||
if (cycleErr != null)
|
||||
return cycleErr;
|
||||
|
||||
ISubjectTransformer? transform = null;
|
||||
var usePublishedSubject = false;
|
||||
if (SubscriptionIndex.SubjectHasWildcard(from))
|
||||
{
|
||||
if (to == from)
|
||||
{
|
||||
usePublishedSubject = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var (created, err) = SubjectTransform.New(from, to);
|
||||
if (err != null)
|
||||
return new InvalidOperationException($"failed to create mapping transform for stream import subject from \"{from}\" to \"{to}\": {err.Message}");
|
||||
transform = created;
|
||||
}
|
||||
}
|
||||
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (IsStreamImportDuplicate(account, from))
|
||||
return ServerErrors.ErrStreamImportDuplicate;
|
||||
|
||||
Imports.Streams ??= [];
|
||||
Imports.Streams.Add(new StreamImportEntry
|
||||
{
|
||||
Account = account,
|
||||
From = from,
|
||||
To = to,
|
||||
Transform = transform,
|
||||
Claim = importClaim,
|
||||
UsePublishedSubject = usePublishedSubject,
|
||||
AllowTrace = allowTrace,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if stream import duplicate exists. Lock should be held.
|
||||
/// Mirrors Go <c>(a *Account) isStreamImportDuplicate(...)</c>.
|
||||
/// </summary>
|
||||
internal bool IsStreamImportDuplicate(Account account, string from)
|
||||
{
|
||||
if (Imports.Streams == null)
|
||||
return false;
|
||||
|
||||
foreach (var streamImport in Imports.Streams)
|
||||
{
|
||||
if (ReferenceEquals(streamImport.Account, account) && streamImport.From == from)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds stream import from a specific account.
|
||||
/// Mirrors Go <c>(a *Account) AddStreamImport(...)</c>.
|
||||
/// </summary>
|
||||
public Exception? AddStreamImport(Account account, string from, string prefix) =>
|
||||
AddStreamImportWithClaimInternal(account, from, prefix, false, null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds stream export, optionally restricted to explicit accounts.
|
||||
/// Mirrors Go <c>(a *Account) AddStreamExport(...)</c>.
|
||||
/// </summary>
|
||||
public Exception? AddStreamExport(string subject, IReadOnlyList<Account>? accounts = null) =>
|
||||
AddStreamExportWithAccountPos(subject, accounts, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Adds stream export with account-position matching.
|
||||
/// Mirrors Go <c>(a *Account) addStreamExportWithAccountPos(...)</c>.
|
||||
/// </summary>
|
||||
public Exception? AddStreamExportWithAccountPos(string subject, IReadOnlyList<Account>? accounts, uint accountPos)
|
||||
{
|
||||
if (!SubscriptionIndex.IsValidSubject(subject))
|
||||
return ServerErrors.ErrBadSubject;
|
||||
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
Exports.Streams ??= new Dictionary<string, StreamExport>(StringComparer.Ordinal);
|
||||
Exports.Streams.TryGetValue(subject, out var export);
|
||||
export ??= new StreamExport();
|
||||
|
||||
if (accounts != null || accountPos > 0)
|
||||
{
|
||||
var authErr = SetExportAuth(export, subject, accounts, accountPos);
|
||||
if (authErr != null)
|
||||
return authErr;
|
||||
}
|
||||
|
||||
Exports.Streams[subject] = export;
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks stream import authorization with account lock.
|
||||
/// Mirrors Go <c>(a *Account) checkStreamImportAuthorized(...)</c>.
|
||||
/// </summary>
|
||||
internal bool CheckStreamImportAuthorized(Account account, string subject, object? importClaim)
|
||||
{
|
||||
_mu.EnterReadLock();
|
||||
try { return CheckStreamImportAuthorizedNoLock(account, subject, importClaim); }
|
||||
finally { _mu.ExitReadLock(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks stream import authorization assuming lock is already held.
|
||||
/// Mirrors Go <c>(a *Account) checkStreamImportAuthorizedNoLock(...)</c>.
|
||||
/// </summary>
|
||||
internal bool CheckStreamImportAuthorizedNoLock(Account account, string subject, object? importClaim)
|
||||
{
|
||||
if (Exports.Streams == null || !SubscriptionIndex.IsValidSubject(subject))
|
||||
return false;
|
||||
return CheckStreamExportApproved(account, subject, importClaim);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets wildcard-matching service export for subject.
|
||||
/// Lock should be held.
|
||||
/// Mirrors Go <c>(a *Account) getWildcardServiceExport(from string)</c>.
|
||||
/// </summary>
|
||||
internal ServiceExportEntry? GetWildcardServiceExport(string from)
|
||||
{
|
||||
if (Exports.Services == null)
|
||||
return null;
|
||||
|
||||
var tokens = SubjectTransform.TokenizeSubject(from);
|
||||
foreach (var (subject, serviceExport) in Exports.Services)
|
||||
{
|
||||
if (SubjectTransform.IsSubsetMatch(tokens, subject))
|
||||
return serviceExport;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles stream import activation expiration.
|
||||
/// Mirrors Go <c>(a *Account) streamActivationExpired(...)</c>.
|
||||
/// </summary>
|
||||
internal void StreamActivationExpired(Account exportAccount, string subject)
|
||||
{
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (IsExpired() || Imports.Streams == null)
|
||||
return;
|
||||
|
||||
foreach (var streamImport in Imports.Streams)
|
||||
{
|
||||
if (ReferenceEquals(streamImport.Account, exportAccount) && streamImport.From == subject)
|
||||
{
|
||||
streamImport.Invalid = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles service import activation expiration.
|
||||
/// Mirrors Go <c>(a *Account) serviceActivationExpired(...)</c>.
|
||||
/// </summary>
|
||||
internal void ServiceActivationExpired(Account destinationAccount, string subject)
|
||||
{
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (IsExpired() || Imports.Services == null)
|
||||
return;
|
||||
|
||||
var serviceImport = GetServiceImportForAccountLocked(destinationAccount.Name, subject);
|
||||
if (serviceImport != null)
|
||||
serviceImport.Invalid = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mu.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Export checks
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
Reference in New Issue
Block a user