411 lines
14 KiB
C#
Raw Normal View History

2025-01-16 11:31:50 +01:00
using CodexClient.Hooks;
using FileUtils;
2024-03-26 11:39:59 +01:00
using Logging;
2023-08-02 15:11:27 +02:00
using Utils;
2023-04-13 09:33:10 +02:00
2025-01-16 11:31:50 +01:00
namespace CodexClient
2023-04-13 09:33:10 +02:00
{
2025-01-16 15:13:16 +01:00
public partial interface ICodexNode : IHasEthAddress, IHasMetricsScrapeTarget
2023-04-13 09:33:10 +02:00
{
string GetName();
string GetImageName();
string GetPeerId();
DebugInfo GetDebugInfo(bool log = false);
2025-03-03 16:44:04 +01:00
void SetLogLevel(string logLevel);
2024-11-21 14:17:57 +01:00
string GetSpr();
2024-03-25 15:46:45 +01:00
DebugPeer GetDebugPeer(string peerId);
2023-09-12 13:32:06 +02:00
ContentId UploadFile(TrackedFile file);
2025-01-16 11:31:50 +01:00
ContentId UploadFile(TrackedFile file, string contentType, string contentDisposition);
2023-09-12 13:32:06 +02:00
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
LocalDataset DownloadStreamless(ContentId cid);
2024-12-18 12:47:54 +01:00
/// <summary>
/// TODO: This will monitor the quota-used of the node until 'size' bytes are added. That's a very bad way
/// to track the streamless download progress. Replace it once we have a good API for this.
/// </summary>
LocalDataset DownloadStreamlessWait(ContentId cid, ByteSize size);
LocalDataset DownloadManifestOnly(ContentId cid);
2024-03-26 14:07:06 +01:00
LocalDatasetList LocalFiles();
CodexSpace Space();
2023-09-19 11:51:59 +02:00
void ConnectToPeer(ICodexNode node);
2024-03-26 10:03:52 +01:00
DebugInfoVersion Version { get; }
2023-09-20 08:45:55 +02:00
IMarketplaceAccess Marketplace { get; }
2023-12-06 09:59:45 +01:00
ITransferSpeeds TransferSpeeds { get; }
2024-05-24 15:34:42 +02:00
EthAccount EthAccount { get; }
2025-03-03 16:27:10 +01:00
StoragePurchase? GetPurchaseStatus(string purchaseId);
2024-06-13 08:51:52 +02:00
Address GetDiscoveryEndpoint();
Address GetApiEndpoint();
Address GetListenEndpoint();
2024-06-13 08:51:52 +02:00
/// <summary>
/// Warning! The node is not usable after this.
/// TODO: Replace with delete-blocks debug call once available in Codex.
/// </summary>
void DeleteDataDirFolder();
void Stop(bool waitTillStopped);
2025-01-16 10:15:02 +01:00
IDownloadedLog DownloadLog(string additionalName = "");
bool HasCrashed();
2023-04-13 09:33:10 +02:00
}
2023-09-19 11:51:59 +02:00
public class CodexNode : ICodexNode
2023-04-13 09:33:10 +02:00
{
private const string UploadFailedMessage = "Unable to store block";
private readonly ILog log;
private readonly ICodexNodeHooks hooks;
2023-12-06 09:59:45 +01:00
private readonly TransferSpeeds transferSpeeds;
private string peerId = string.Empty;
private string nodeId = string.Empty;
private readonly CodexAccess codexAccess;
2025-01-16 11:31:50 +01:00
private readonly IFileManager fileManager;
2023-04-13 09:33:10 +02:00
2025-01-16 11:31:50 +01:00
public CodexNode(ILog log, CodexAccess codexAccess, IFileManager fileManager, IMarketplaceAccess marketplaceAccess, ICodexNodeHooks hooks)
2023-04-13 09:33:10 +02:00
{
this.codexAccess = codexAccess;
2025-01-16 11:31:50 +01:00
this.fileManager = fileManager;
2023-09-20 08:45:55 +02:00
Marketplace = marketplaceAccess;
this.hooks = hooks;
2024-03-26 10:03:52 +01:00
Version = new DebugInfoVersion();
2023-12-06 09:59:45 +01:00
transferSpeeds = new TransferSpeeds();
2025-01-16 11:31:50 +01:00
this.log = new LogPrefixer(log, $"{GetName()} ");
2023-04-13 09:33:10 +02:00
}
public void Awake()
{
2025-01-15 15:43:50 +01:00
hooks.OnNodeStarting(codexAccess.GetStartUtc(), codexAccess.GetImageName(), codexAccess.GetEthAccount());
}
public void Initialize()
{
2025-01-15 15:43:50 +01:00
InitializePeerNodeId();
InitializeLogReplacements();
2025-01-29 14:44:19 +01:00
hooks.OnNodeStarted(this, peerId, nodeId);
}
2023-09-20 08:45:55 +02:00
public IMarketplaceAccess Marketplace { get; }
2024-03-26 10:03:52 +01:00
public DebugInfoVersion Version { get; private set; }
2023-12-06 09:59:45 +01:00
public ITransferSpeeds TransferSpeeds { get => transferSpeeds; }
2024-03-26 10:03:52 +01:00
2025-03-03 16:27:10 +01:00
public StoragePurchase? GetPurchaseStatus(string purchaseId)
2025-01-16 15:13:16 +01:00
{
return codexAccess.GetPurchaseStatus(purchaseId);
}
2023-09-20 10:13:29 +02:00
public EthAddress EthAddress
2023-09-19 11:51:59 +02:00
{
get
{
2024-05-24 15:34:42 +02:00
EnsureMarketplace();
2025-01-15 15:43:50 +01:00
return codexAccess.GetEthAccount()!.EthAddress;
2024-05-24 15:34:42 +02:00
}
}
public EthAccount EthAccount
{
get
{
EnsureMarketplace();
2025-01-15 15:43:50 +01:00
return codexAccess.GetEthAccount()!;
2023-09-19 11:51:59 +02:00
}
}
2023-04-13 09:33:10 +02:00
public string GetName()
{
return codexAccess.GetName();
2023-04-13 09:33:10 +02:00
}
public string GetImageName()
{
return codexAccess.GetImageName();
}
public string GetPeerId()
{
return peerId;
}
public DebugInfo GetDebugInfo(bool log = false)
2023-04-13 09:33:10 +02:00
{
var debugInfo = codexAccess.GetDebugInfo();
if (log)
{
var known = string.Join(",", debugInfo.Table.Nodes.Select(n => n.PeerId));
Log($"Got DebugInfo with id: {debugInfo.Id}. This node knows: [{known}]");
}
2023-04-19 14:57:00 +02:00
return debugInfo;
2023-04-13 09:33:10 +02:00
}
2025-03-03 16:44:04 +01:00
public void SetLogLevel(string logLevel)
{
codexAccess.SetLogLevel(logLevel);
}
2024-11-21 14:17:57 +01:00
public string GetSpr()
{
return codexAccess.GetSpr();
2024-11-21 14:17:57 +01:00
}
2024-03-25 15:46:45 +01:00
public DebugPeer GetDebugPeer(string peerId)
2023-05-11 12:44:53 +02:00
{
return codexAccess.GetDebugPeer(peerId);
2023-05-11 12:44:53 +02:00
}
2023-09-12 13:32:06 +02:00
public ContentId UploadFile(TrackedFile file)
2023-04-13 09:33:10 +02:00
{
2025-01-16 11:31:50 +01:00
return UploadFile(file, "application/octet-stream", $"attachment; filename=\"{Path.GetFileName(file.Filename)}\"");
}
2025-01-16 11:31:50 +01:00
public ContentId UploadFile(TrackedFile file, string contentType, string contentDisposition)
2023-04-13 09:33:10 +02:00
{
2023-09-12 11:43:46 +02:00
using var fileStream = File.OpenRead(file.Filename);
var uniqueId = Guid.NewGuid().ToString();
var size = file.GetFilesize();
hooks.OnFileUploading(uniqueId, size);
2024-10-30 08:34:41 +01:00
var input = new UploadInput(contentType, contentDisposition, fileStream);
var logMessage = $"Uploading file {file.Describe()} with contentType: '{input.ContentType}' and disposition: '{input.ContentDisposition}'...";
var measurement = Stopwatch.Measure(log, logMessage, () =>
2023-09-12 11:43:46 +02:00
{
2025-01-16 11:31:50 +01:00
return codexAccess.UploadFile(input);
2023-09-12 11:43:46 +02:00
});
2023-12-06 09:59:45 +01:00
var response = measurement.Value;
transferSpeeds.AddUploadSample(size, measurement.Duration);
2023-12-06 09:59:45 +01:00
2023-09-20 10:51:47 +02:00
if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response.");
if (response.StartsWith(UploadFailedMessage)) FrameworkAssert.Fail("Node failed to store block.");
2023-07-11 08:19:14 +02:00
2024-08-01 14:50:25 +02:00
Log($"Uploaded file {file.Describe()}. Received contentId: '{response}'.");
2024-06-12 15:28:08 +02:00
var cid = new ContentId(response);
hooks.OnFileUploaded(uniqueId, size, cid);
return cid;
2023-04-13 09:33:10 +02:00
}
2023-09-12 13:32:06 +02:00
public TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "")
{
2025-01-16 11:31:50 +01:00
var file = fileManager.CreateEmptyFile(fileLabel);
hooks.OnFileDownloading(contentId);
Log($"Downloading '{contentId}'...");
var logMessage = $"Downloaded '{contentId}' to '{file.Filename}'";
2025-01-16 11:31:50 +01:00
var measurement = Stopwatch.Measure(log, logMessage, () => DownloadToFile(contentId.Id, file));
var size = file.GetFilesize();
transferSpeeds.AddDownloadSample(size, measurement);
hooks.OnFileDownloaded(size, contentId);
2023-09-12 11:43:46 +02:00
return file;
2023-04-13 09:33:10 +02:00
}
public LocalDataset DownloadStreamless(ContentId cid)
{
2024-12-18 12:47:54 +01:00
Log($"Downloading streamless '{cid}' (no-wait)");
return codexAccess.DownloadStreamless(cid);
}
2024-12-18 12:47:54 +01:00
public LocalDataset DownloadStreamlessWait(ContentId cid, ByteSize size)
{
Log($"Downloading streamless '{cid}' (wait till finished)");
var sw = Stopwatch.Measure(log, nameof(DownloadStreamlessWait), () =>
{
var startSpace = Space();
var result = codexAccess.DownloadStreamless(cid);
2024-12-18 12:47:54 +01:00
WaitUntilQuotaUsedIncreased(startSpace, size);
return result;
});
return sw.Value;
}
public LocalDataset DownloadManifestOnly(ContentId cid)
{
2024-12-18 12:47:54 +01:00
Log($"Downloading manifest-only '{cid}'");
return codexAccess.DownloadManifestOnly(cid);
}
2024-03-26 14:07:06 +01:00
public LocalDatasetList LocalFiles()
2023-11-10 08:20:08 +01:00
{
return codexAccess.LocalFiles();
2023-11-10 08:20:08 +01:00
}
public CodexSpace Space()
{
return codexAccess.Space();
}
2023-09-19 11:51:59 +02:00
public void ConnectToPeer(ICodexNode node)
2023-04-13 09:33:10 +02:00
{
2023-09-19 11:51:59 +02:00
var peer = (CodexNode)node;
2023-04-13 09:33:10 +02:00
Log($"Connecting to peer {peer.GetName()}...");
var peerInfo = node.GetDebugInfo();
codexAccess.ConnectToPeer(peerInfo.Id, GetPeerMultiAddresses(peer, peerInfo));
2023-04-13 09:33:10 +02:00
Log($"Successfully connected to peer {peer.GetName()}.");
}
public void DeleteDataDirFolder()
2024-06-13 08:51:52 +02:00
{
codexAccess.DeleteDataDirFolder();
2024-06-13 08:51:52 +02:00
}
public void Stop(bool waitTillStopped)
2023-04-25 12:52:11 +02:00
{
2024-06-12 15:28:08 +02:00
Log("Stopping...");
hooks.OnNodeStopping();
2025-01-15 15:43:50 +01:00
codexAccess.Stop(waitTillStopped);
2023-07-31 11:51:29 +02:00
}
2025-01-16 10:15:02 +01:00
public IDownloadedLog DownloadLog(string additionalName = "")
{
return codexAccess.DownloadLog(additionalName);
}
public Address GetDiscoveryEndpoint()
{
return codexAccess.GetDiscoveryEndpoint();
}
public Address GetApiEndpoint()
{
return codexAccess.GetApiEndpoint();
}
public Address GetListenEndpoint()
{
return codexAccess.GetListenEndpoint();
}
2025-01-16 13:24:57 +01:00
public Address GetMetricsScrapeTarget()
{
var address = codexAccess.GetMetricsEndpoint();
if (address == null) throw new Exception("Metrics ScrapeTarget accessed, but node was not started with EnableMetrics()");
return address;
}
public bool HasCrashed()
{
2025-01-16 13:24:57 +01:00
return codexAccess.HasCrashed();
}
public override string ToString()
{
return $"CodexNode:{GetName()}";
}
2025-01-15 15:43:50 +01:00
private void InitializePeerNodeId()
{
var debugInfo = Time.Retry(codexAccess.GetDebugInfo, "ensure online");
if (!debugInfo.Version.IsValid())
{
throw new Exception($"Invalid version information received from Codex node {GetName()}: {debugInfo.Version}");
}
peerId = debugInfo.Id;
nodeId = debugInfo.Table.LocalNode.NodeId;
Version = debugInfo.Version;
}
private void InitializeLogReplacements()
{
var nodeName = GetName();
log.AddStringReplace(peerId, nodeName);
log.AddStringReplace(CodexUtils.ToShortId(peerId), nodeName);
log.AddStringReplace(nodeId, nodeName);
log.AddStringReplace(CodexUtils.ToShortId(nodeId), nodeName);
}
2024-03-26 12:14:02 +01:00
private string[] GetPeerMultiAddresses(CodexNode peer, DebugInfo peerInfo)
2023-04-13 09:33:10 +02:00
{
var peerId = peer.GetDiscoveryEndpoint().Host
.Replace("http://", "")
.Replace("https://", "");
2024-03-26 12:14:02 +01:00
return peerInfo.Addrs.Select(a => a
.Replace("0.0.0.0", peerId))
2024-03-26 12:14:02 +01:00
.ToArray();
2023-04-13 09:33:10 +02:00
}
2025-01-16 11:31:50 +01:00
private void DownloadToFile(string contentId, TrackedFile file)
2023-04-13 09:33:10 +02:00
{
using var fileStream = File.OpenWrite(file.Filename);
2025-01-16 11:31:50 +01:00
var timeout = TimeSpan.FromMinutes(2.0); // todo: make this user-controllable.
2023-04-19 14:57:00 +02:00
try
{
// Type of stream generated by openAPI client does not support timeouts.
var start = DateTime.UtcNow;
var cts = new CancellationTokenSource();
2025-01-16 11:31:50 +01:00
var downloadTask = Task.Run(() =>
{
2025-01-16 11:31:50 +01:00
using var downloadStream = codexAccess.DownloadFile(contentId);
downloadStream.CopyTo(fileStream);
}, cts.Token);
while (DateTime.UtcNow - start < timeout)
{
if (downloadTask.IsFaulted) throw downloadTask.Exception;
if (downloadTask.IsCompletedSuccessfully) return;
Thread.Sleep(100);
}
cts.Cancel();
throw new TimeoutException($"Download of '{contentId}' timed out after {Time.FormatDuration(timeout)}");
2023-04-19 14:57:00 +02:00
}
2025-01-29 14:44:19 +01:00
catch (Exception ex)
2023-04-19 14:57:00 +02:00
{
2025-01-29 14:44:19 +01:00
Log($"Failed to download file '{contentId}': {ex}");
2023-04-19 14:57:00 +02:00
throw;
}
2023-04-13 09:33:10 +02:00
}
2024-12-18 12:47:54 +01:00
public void WaitUntilQuotaUsedIncreased(CodexSpace startSpace, ByteSize expectedIncreaseOfQuotaUsed)
{
WaitUntilQuotaUsedIncreased(startSpace, expectedIncreaseOfQuotaUsed, TimeSpan.FromMinutes(2));
}
public void WaitUntilQuotaUsedIncreased(
CodexSpace startSpace,
ByteSize expectedIncreaseOfQuotaUsed,
TimeSpan maxTimeout)
{
Log($"Waiting until quotaUsed " +
$"(start: {startSpace.QuotaUsedBytes}) " +
$"increases by {expectedIncreaseOfQuotaUsed} " +
$"to reach {startSpace.QuotaUsedBytes + expectedIncreaseOfQuotaUsed.SizeInBytes}");
var retry = new Retry($"Checking local space for quotaUsed increase of {expectedIncreaseOfQuotaUsed}",
maxTimeout: maxTimeout,
2025-04-08 13:07:55 +02:00
sleepAfterFail: TimeSpan.FromSeconds(10),
onFail: f => { },
failFast: false);
2024-12-18 12:47:54 +01:00
retry.Run(() =>
{
var space = Space();
var increase = space.QuotaUsedBytes - startSpace.QuotaUsedBytes;
if (increase < expectedIncreaseOfQuotaUsed.SizeInBytes)
throw new Exception($"Expected quota-used not reached. " +
$"Expected increase: {expectedIncreaseOfQuotaUsed.SizeInBytes} " +
$"Actual increase: {increase} " +
$"Actual used: {space.QuotaUsedBytes}");
});
}
2024-05-24 15:34:42 +02:00
private void EnsureMarketplace()
{
2025-01-15 15:43:50 +01:00
if (codexAccess.GetEthAccount() == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it.");
2024-05-24 15:34:42 +02:00
}
2023-04-13 09:33:10 +02:00
private void Log(string msg)
{
log.Log(msg);
2023-04-13 09:33:10 +02:00
}
}
}