2025-01-16 11:31:50 +01:00
using CodexClient.Hooks ;
2023-09-11 10:43:27 +02:00
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
{
2023-04-19 09:19:06 +02:00
string GetName ( ) ;
2025-01-15 16:05:57 +01:00
string GetImageName ( ) ;
2024-07-26 08:39:27 +02:00
string GetPeerId ( ) ;
2024-12-02 10:34:09 +01:00
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 = "" ) ;
2024-11-21 11:28:18 +01:00
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 ) ;
2024-11-21 11:28:18 +01:00
LocalDataset DownloadManifestOnly ( ContentId cid ) ;
2024-03-26 14:07:06 +01:00
LocalDatasetList LocalFiles ( ) ;
2024-06-05 09:20:00 +02:00
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
2025-01-15 14:15:40 +01:00
Address GetDiscoveryEndpoint ( ) ;
2025-01-15 16:05:57 +01:00
Address GetApiEndpoint ( ) ;
Address GetListenEndpoint ( ) ;
2025-01-15 14:15:40 +01:00
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>
2025-01-15 15:00:25 +01:00
void DeleteDataDirFolder ( ) ;
2024-03-13 10:57:26 +01:00
void Stop ( bool waitTillStopped ) ;
2025-01-16 10:15:02 +01:00
IDownloadedLog DownloadLog ( string additionalName = "" ) ;
2025-01-15 14:15:40 +01:00
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" ;
2024-08-01 10:39:06 +02:00
private readonly ILog log ;
2024-07-26 08:39:27 +02:00
private readonly ICodexNodeHooks hooks ;
2023-12-06 09:59:45 +01:00
private readonly TransferSpeeds transferSpeeds ;
2024-07-26 08:39:27 +02:00
private string peerId = string . Empty ;
2024-08-13 13:48:54 +02:00
private string nodeId = string . Empty ;
2025-01-15 15:00:25 +01:00
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
{
2025-01-15 15:00:25 +01: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 ;
2024-07-26 08:39:27 +02:00
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 ( ) ;
2024-08-01 10:39:06 +02:00
2025-01-16 11:31:50 +01:00
this . log = new LogPrefixer ( log , $"{GetName()} " ) ;
2023-04-13 09:33:10 +02:00
}
2024-07-29 10:16:37 +02:00
public void Awake ( )
{
2025-01-15 15:43:50 +01:00
hooks . OnNodeStarting ( codexAccess . GetStartUtc ( ) , codexAccess . GetImageName ( ) , codexAccess . GetEthAccount ( ) ) ;
2024-07-29 10:16:37 +02:00
}
2024-07-26 08:39:27 +02:00
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 ) ;
2024-07-26 08:39:27 +02:00
}
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 ( )
{
2025-01-15 15:00:25 +01:00
return codexAccess . GetName ( ) ;
2023-04-13 09:33:10 +02:00
}
2025-01-15 16:05:57 +01:00
public string GetImageName ( )
{
return codexAccess . GetImageName ( ) ;
}
2024-07-26 08:39:27 +02:00
public string GetPeerId ( )
{
return peerId ;
}
2024-12-02 10:34:09 +01:00
public DebugInfo GetDebugInfo ( bool log = false )
2023-04-13 09:33:10 +02:00
{
2025-01-15 15:00:25 +01:00
var debugInfo = codexAccess . GetDebugInfo ( ) ;
2024-12-02 10:34:09 +01:00
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 ( )
{
2025-01-15 15:00:25 +01:00
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
{
2025-01-15 15:00:25 +01: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 ) } \ "" ) ;
2024-06-06 09:54:50 +02:00
}
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 ) ;
2024-08-01 09:09:30 +02:00
var uniqueId = Guid . NewGuid ( ) . ToString ( ) ;
var size = file . GetFilesize ( ) ;
hooks . OnFileUploading ( uniqueId , size ) ;
2023-06-07 08:30:10 +02:00
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}'..." ;
2024-08-01 10:39:06 +02:00
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-06-07 08:30:10 +02:00
2023-12-06 09:59:45 +01:00
var response = measurement . Value ;
2024-08-01 09:09:30 +02:00
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
2024-07-26 08:39:27 +02:00
var cid = new ContentId ( response ) ;
2024-08-01 09:09:30 +02:00
hooks . OnFileUploaded ( uniqueId , size , cid ) ;
2024-07-26 08:39:27 +02:00
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 = "" )
2024-06-06 09:54:50 +02:00
{
2025-01-16 11:31:50 +01:00
var file = fileManager . CreateEmptyFile ( fileLabel ) ;
2024-08-01 10:39:06 +02:00
hooks . OnFileDownloading ( contentId ) ;
2024-08-01 11:19:05 +02:00
Log ( $"Downloading '{contentId}'..." ) ;
2024-08-01 10:39:06 +02:00
2024-08-01 11:19:05 +02:00
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 ) ) ;
2024-08-01 10:39:06 +02:00
2024-08-01 09:09:30 +02:00
var size = file . GetFilesize ( ) ;
transferSpeeds . AddDownloadSample ( size , measurement ) ;
hooks . OnFileDownloaded ( size , contentId ) ;
2024-08-01 10:39:06 +02:00
2023-09-12 11:43:46 +02:00
return file ;
2023-04-13 09:33:10 +02:00
}
2024-11-21 11:28:18 +01:00
public LocalDataset DownloadStreamless ( ContentId cid )
{
2024-12-18 12:47:54 +01:00
Log ( $"Downloading streamless '{cid}' (no-wait)" ) ;
2025-01-15 15:00:25 +01:00
return codexAccess . DownloadStreamless ( cid ) ;
2024-11-21 11:28:18 +01:00
}
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 ( ) ;
2025-01-15 15:00:25 +01:00
var result = codexAccess . DownloadStreamless ( cid ) ;
2024-12-18 12:47:54 +01:00
WaitUntilQuotaUsedIncreased ( startSpace , size ) ;
return result ;
} ) ;
return sw . Value ;
}
2024-11-21 11:28:18 +01:00
public LocalDataset DownloadManifestOnly ( ContentId cid )
{
2024-12-18 12:47:54 +01:00
Log ( $"Downloading manifest-only '{cid}'" ) ;
2025-01-15 15:00:25 +01:00
return codexAccess . DownloadManifestOnly ( cid ) ;
2024-11-21 11:28:18 +01:00
}
2024-03-26 14:07:06 +01:00
public LocalDatasetList LocalFiles ( )
2023-11-10 08:20:08 +01:00
{
2025-01-15 15:00:25 +01:00
return codexAccess . LocalFiles ( ) ;
2023-11-10 08:20:08 +01:00
}
2024-06-05 09:20:00 +02:00
public CodexSpace Space ( )
{
2025-01-15 15:00:25 +01:00
return codexAccess . Space ( ) ;
2024-06-05 09:20:00 +02:00
}
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 ( ) ;
2025-01-15 15:00:25 +01:00
codexAccess . ConnectToPeer ( peerInfo . Id , GetPeerMultiAddresses ( peer , peerInfo ) ) ;
2023-04-13 09:33:10 +02:00
Log ( $"Successfully connected to peer {peer.GetName()}." ) ;
}
2025-01-15 15:00:25 +01:00
public void DeleteDataDirFolder ( )
2024-06-13 08:51:52 +02:00
{
2025-01-15 15:00:25 +01:00
codexAccess . DeleteDataDirFolder ( ) ;
2024-06-13 08:51:52 +02:00
}
2024-03-13 10:57:26 +01:00
public void Stop ( bool waitTillStopped )
2023-04-25 12:52:11 +02:00
{
2024-06-12 15:28:08 +02:00
Log ( "Stopping..." ) ;
2024-07-26 08:39:27 +02:00
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 ) ;
}
2025-01-15 14:15:40 +01:00
public Address GetDiscoveryEndpoint ( )
{
2025-01-15 15:00:25 +01:00
return codexAccess . GetDiscoveryEndpoint ( ) ;
2025-01-15 14:15:40 +01:00
}
2025-01-15 16:05:57 +01:00
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 ;
}
2025-01-15 14:15:40 +01:00
public bool HasCrashed ( )
{
2025-01-16 13:24:57 +01:00
return codexAccess . HasCrashed ( ) ;
2025-01-15 14:15:40 +01:00
}
2024-09-24 10:10:59 +02:00
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
{
2025-01-15 15:00:25 +01:00
var peerId = peer . GetDiscoveryEndpoint ( ) . Host
. Replace ( "http://" , "" )
. Replace ( "https://" , "" ) ;
2023-11-06 14:33:47 +01:00
2024-03-26 12:14:02 +01:00
return peerInfo . Addrs . Select ( a = > a
2025-01-15 15:00:25 +01:00
. 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
{
2024-10-25 12:39:10 +02:00
// 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 ( ( ) = >
2024-10-25 12:39:10 +02:00
{
2025-01-16 11:31:50 +01:00
using var downloadStream = codexAccess . DownloadFile ( contentId ) ;
downloadStream . CopyTo ( fileStream ) ;
} , cts . Token ) ;
2024-10-25 12:39:10 +02:00
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 )
{
2024-08-01 10:39:06 +02:00
log . Log ( msg ) ;
2023-04-13 09:33:10 +02:00
}
}
}