From 4a8510f9182b997e226010cf262a1212aa0a8310 Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Thu, 11 Apr 2024 20:05:12 +0200 Subject: [PATCH 01/18] Multi activity script --- Golem.Tools/App/app.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Golem.Tools/App/app.py b/Golem.Tools/App/app.py index d4a63dd0..16ff3cc2 100644 --- a/Golem.Tools/App/app.py +++ b/Golem.Tools/App/app.py @@ -3,7 +3,7 @@ import json from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta, timezone from yapapi import Golem from yapapi.payload import Payload @@ -15,7 +15,6 @@ import argparse import asyncio import tempfile -from datetime import datetime, timezone from pathlib import Path from typing import Optional @@ -195,6 +194,18 @@ def __init__(self, strategy: ProviderOnceStrategy): self.strategy = strategy +async def reset_activities(golem, cluster): + for instance in cluster.instances: + if instance._ctx != None: + print(f"Resetting activity for {instance.provider_name}") + + activity = await golem._engine._activity_api.use_activity(instance._ctx._activity.id) + await activity.destroy() + + await asyncio.sleep(2) + await golem._engine._activity_api.new_activity(instance._ctx._agreement.id) + + async def main(subnet_tag, descriptor, driver=None, network=None, runtime="dummy"): strategy = ProviderOnceStrategy() async with Golem( @@ -212,6 +223,7 @@ async def main(subnet_tag, descriptor, driver=None, network=None, runtime="dummy {"strategy": strategy} ], num_instances=1, + expiration=datetime.now(timezone.utc) + timedelta(days=10), ) async def print_usage(): @@ -264,8 +276,8 @@ def instances(): } for s in cluster.instances ] - usage_printed = False + reset_counter = 0 while True: await asyncio.sleep(3) @@ -277,6 +289,12 @@ def instances(): usage_printed = True print(f"""instances: {[f"{r['name']}: {r['state']}" for r in i]}""") + + reset_counter = reset_counter + 1 + if reset_counter > 5: + await reset_activities(golem, cluster) + reset_counter = 0 + if __name__ == "__main__": parser = build_parser("Run AI runtime task") From d9fb4ea84cbaa6567b9df78866e102786bd9f43f Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Mon, 15 Apr 2024 18:07:17 +0200 Subject: [PATCH 02/18] Replace restarting activity with more often DebitNotes configuration --- Golem.Tools/App/app.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/Golem.Tools/App/app.py b/Golem.Tools/App/app.py index 16ff3cc2..e3cb53bf 100644 --- a/Golem.Tools/App/app.py +++ b/Golem.Tools/App/app.py @@ -25,6 +25,7 @@ from yapapi import windows_event_loop_fix from yapapi.log import enable_default_logger from yapapi.strategy import SCORE_TRUSTED, SCORE_REJECTED, MarketStrategy +from yapapi.strategy.base import PropValueRange, PROP_DEBIT_NOTE_INTERVAL_SEC, PROP_PAYMENT_TIMEOUT_SEC # Utils @@ -132,6 +133,10 @@ class ProviderOnceStrategy(MarketStrategy): def __init__(self): self.history = set(()) + self.acceptable_prop_value_range_overrides = { + PROP_DEBIT_NOTE_INTERVAL_SEC: PropValueRange(60, None), + PROP_PAYMENT_TIMEOUT_SEC: PropValueRange(180, None), + } async def score_offer(self, offer): if offer.issuer not in self.history: @@ -192,24 +197,12 @@ async def start(self): def __init__(self, strategy: ProviderOnceStrategy): super().__init__() self.strategy = strategy - - -async def reset_activities(golem, cluster): - for instance in cluster.instances: - if instance._ctx != None: - print(f"Resetting activity for {instance.provider_name}") - - activity = await golem._engine._activity_api.use_activity(instance._ctx._activity.id) - await activity.destroy() - - await asyncio.sleep(2) - await golem._engine._activity_api.new_activity(instance._ctx._agreement.id) async def main(subnet_tag, descriptor, driver=None, network=None, runtime="dummy"): strategy = ProviderOnceStrategy() async with Golem( - budget=1.0, + budget=4.0, subnet_tag=subnet_tag, strategy=strategy, payment_driver=driver, @@ -277,7 +270,6 @@ def instances(): ] usage_printed = False - reset_counter = 0 while True: await asyncio.sleep(3) @@ -289,11 +281,6 @@ def instances(): usage_printed = True print(f"""instances: {[f"{r['name']}: {r['state']}" for r in i]}""") - - reset_counter = reset_counter + 1 - if reset_counter > 5: - await reset_activities(golem, cluster) - reset_counter = 0 if __name__ == "__main__": From 6389faef2c6becb0c2d9e0744b3180e8435a81f1 Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Mon, 15 Apr 2024 18:40:27 +0200 Subject: [PATCH 03/18] Filter Jobs by timestamp --- Golem/InvoiceEventsLoop.cs | 17 ++++++++------ Golem/Jobs.cs | 46 +++++++++++++++++++++++--------------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/Golem/InvoiceEventsLoop.cs b/Golem/InvoiceEventsLoop.cs index af444ab3..da4ef467 100644 --- a/Golem/InvoiceEventsLoop.cs +++ b/Golem/InvoiceEventsLoop.cs @@ -1,5 +1,7 @@ using Golem.Yagna; + using GolemLib.Types; + using Microsoft.Extensions.Logging; @@ -10,7 +12,7 @@ class InvoiceEventsLoop private readonly ILogger _logger; private readonly IJobs _jobs; private DateTime _since = DateTime.MinValue; - + public InvoiceEventsLoop(YagnaApi yagnaApi, CancellationToken token, ILogger logger, IJobs jobs) { @@ -27,7 +29,7 @@ public async Task Start() DateTime newReconnect = DateTime.Now; await Task.Yield(); - + while (!_token.IsCancellationRequested) { try @@ -37,30 +39,31 @@ public async Task Start() { _since = invoiceEvents.Max(x => x.EventDate); - foreach(var invoiceEvent in invoiceEvents.OrderBy(e => e.EventDate)) + foreach (var invoiceEvent in invoiceEvents.OrderBy(e => e.EventDate)) { await UpdatesForInvoice(invoiceEvent); } } } - catch(TaskCanceledException) + catch (TaskCanceledException) { _logger.LogInformation("Invoice events loop cancelled"); return; } - catch(Exception e) + catch (Exception e) { _logger.LogError("Error in invoice events loop: {e}", e.Message); await Task.Delay(TimeSpan.FromSeconds(5), _token); } - } + } } private async Task UpdatesForInvoice(InvoiceEvent invoiceEvent) { var invoice = await _yagnaApi.GetInvoice(invoiceEvent.InvoiceId, _token); - foreach(var activityId in invoice.ActivityIds) + _logger.LogDebug("Update Invoice info for Job: {}, status: {}", invoice.AgreementId, invoice.Status); + foreach (var activityId in invoice.ActivityIds) await _jobs.UpdateJob(activityId, invoice, null); } } diff --git a/Golem/Jobs.cs b/Golem/Jobs.cs index 68c8f1fd..595a39df 100644 --- a/Golem/Jobs.cs +++ b/Golem/Jobs.cs @@ -1,10 +1,13 @@ using System.Text.Json; + using Golem.Model; using Golem.Tools; using Golem.Yagna; using Golem.Yagna.Types; + using GolemLib; using GolemLib.Types; + using Microsoft.Extensions.Logging; @@ -49,7 +52,7 @@ public async Task GetOrCreateJob(string jobId) _jobs[jobId] = job; } - + return job; } @@ -67,32 +70,32 @@ public void SetAllJobsFinished() public async Task> List(DateTime since) { - if(_yagna == null || _yagna.HasExited) + if (_yagna == null || _yagna.HasExited) throw new Exception("Invalid state: yagna is not started"); var agreementInfos = await _yagna.Api.GetAgreements(since); var invoices = await _yagna.Api.GetInvoices(since); - foreach(var agreementInfo in agreementInfos.Where(a => a!=null && a.AgreementID!=null)) + foreach (var agreementInfo in agreementInfos.Where(a => a != null && a.AgreementID != null)) { var agreement = await _yagna.Api.GetAgreement(agreementInfo.AgreementID!); var activities = await _yagna.Api.GetActivities(agreement!.AgreementID!); var invoice = invoices.FirstOrDefault(i => i.AgreementId == agreement.AgreementID); - foreach(var activityId in activities) + foreach (var activityId in activities) { var usage = await _yagna.Api.GetActivityUsage(activityId); - if(usage.CurrentUsage != null) + if (usage.CurrentUsage != null) { var price = GetPriceFromAgreementAndUsage(agreement, usage.CurrentUsage); await UpdateJob( activityId, invoice, - price!=null ? new GolemUsage(price) : null); + price != null ? new GolemUsage(price) : null); } - } + } } - return _jobs.Values.Cast().ToList(); + return _jobs.Values.SkipWhile(job => job.Timestamp < since).Cast().ToList(); } private GolemPrice? GetPriceFromAgreement(YagnaAgreement agreement) @@ -111,7 +114,7 @@ await UpdateJob( private GolemPrice? GetPriceFromAgreementAndUsage(YagnaAgreement agreement, List vals) { - if (agreement?.Offer?.Properties != null + if (agreement?.Offer?.Properties != null && agreement.Offer.Properties.TryGetValue("golem.com.usage.vector", out var usageVector) && usageVector != null) { @@ -146,12 +149,12 @@ public async Task UpdateJob(string activityId, Invoice? invoice, GolemUsage { var agreementId = await _yagna.Api.GetActivityAgreement(activityId); await UpdateJobStatus(activityId); - - if(invoice != null) + + if (invoice != null) await UpdateJobPayment(invoice); // update usage only if there was any change - if(usage != null) + if (usage != null) await UpdateJobUsage(agreementId); return await GetOrCreateJob(agreementId); @@ -162,13 +165,20 @@ public async Task UpdateJobPayment(Invoice invoice) { var job = await GetOrCreateJob(invoice.AgreementId); - var payments = await _yagna.Api.GetPayments(null); + var payments = await _yagna.Api.GetPayments(); var paymentsForRecentJob = payments .Where(p => p.AgreementPayments.Exists(ap => ap.AgreementId == invoice.AgreementId) || p.ActivityPayments.Exists(ap => invoice.ActivityIds.Contains(ap.ActivityId))) .ToList(); job.PaymentConfirmation = paymentsForRecentJob; job.PaymentStatus = IntoPaymentStatus(invoice.Status); + // Workaround for yagna unable to change status to SETTLED when using partial payments + if (invoice.Status == InvoiceStatus.ACCEPTED + && job.CurrentReward == job.PaymentConfirmation.Sum(payment => Convert.ToDecimal(payment.Amount))) + { + invoice.Status = InvoiceStatus.SETTLED; + } + return job; } @@ -179,7 +189,7 @@ private async Task UpdateJobStatus(string activityId) var agreement = await _yagna.Api.GetAgreement(agreementId); var job = await GetOrCreateJob(agreementId); - if (activityStatePair != null) + if (activityStatePair != null) job.UpdateActivityState(activityStatePair); // In case activity state wasn't properly updated by Provider or ExeUnit. @@ -195,16 +205,16 @@ private async Task UpdateJobUsage(string agreementId) var job = await GetOrCreateJob(agreementId); var agreement = await _yagna.Api.GetAgreement(agreementId); var activities = await _yagna.Api.GetActivities(agreementId); - + var usage = new GolemUsage(); - foreach(var activity in activities) + foreach (var activity in activities) { var activityUsage = await _yagna.Api.GetActivityUsage(activity); - if(activityUsage.CurrentUsage == null) + if (activityUsage.CurrentUsage == null) continue; var price = GetPriceFromAgreementAndUsage(agreement, activityUsage.CurrentUsage); - if(price == null) + if (price == null) continue; usage += new GolemUsage(price); From e277f5817ab04390ae38222f6bc8a38080a9b89d Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Mon, 15 Apr 2024 18:52:49 +0200 Subject: [PATCH 04/18] Fix filtering by timestamp --- Golem/Jobs.cs | 2 +- MockGUI/GolemModel.cs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Golem/Jobs.cs b/Golem/Jobs.cs index 595a39df..32ef62a9 100644 --- a/Golem/Jobs.cs +++ b/Golem/Jobs.cs @@ -95,7 +95,7 @@ await UpdateJob( } } - return _jobs.Values.SkipWhile(job => job.Timestamp < since).Cast().ToList(); + return _jobs.Values.Where(job => job.Timestamp >= since).Cast().ToList(); } private GolemPrice? GetPriceFromAgreement(YagnaAgreement agreement) diff --git a/MockGUI/GolemModel.cs b/MockGUI/GolemModel.cs index e9dc47a9..82a5b84d 100644 --- a/MockGUI/GolemModel.cs +++ b/MockGUI/GolemModel.cs @@ -80,7 +80,8 @@ static async Task Create(string modulesDir, Func jobs; try { + _logger.LogInformation("Listing jobs since: " + since); jobs = await this.Golem.ListJobs(since); - } catch (Exception) + } + catch (Exception) { jobs = new List(); } - + this.JobsHistory = new ObservableCollection(jobs); } From 7e1dd7dbcc4328639a637b3dbfa71166ca02da1c Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Mon, 15 Apr 2024 18:58:26 +0200 Subject: [PATCH 05/18] GetPayments starting at job timestamp --- Golem/Jobs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Golem/Jobs.cs b/Golem/Jobs.cs index 32ef62a9..761d1164 100644 --- a/Golem/Jobs.cs +++ b/Golem/Jobs.cs @@ -165,7 +165,7 @@ public async Task UpdateJobPayment(Invoice invoice) { var job = await GetOrCreateJob(invoice.AgreementId); - var payments = await _yagna.Api.GetPayments(); + var payments = await _yagna.Api.GetPayments(job.Timestamp); var paymentsForRecentJob = payments .Where(p => p.AgreementPayments.Exists(ap => ap.AgreementId == invoice.AgreementId) || p.ActivityPayments.Exists(ap => invoice.ActivityIds.Contains(ap.ActivityId))) .ToList(); From e7ebc4e96342280c40d613426199b36d275fb6e4 Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Mon, 15 Apr 2024 19:50:45 +0200 Subject: [PATCH 06/18] Refactor to make less calls to api --- Golem/Jobs.cs | 53 ++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/Golem/Jobs.cs b/Golem/Jobs.cs index 761d1164..21ad1258 100644 --- a/Golem/Jobs.cs +++ b/Golem/Jobs.cs @@ -10,6 +10,8 @@ using Microsoft.Extensions.Logging; +using static Golem.Model.ActivityState; + public interface IJobs { @@ -78,21 +80,12 @@ public async Task> List(DateTime since) foreach (var agreementInfo in agreementInfos.Where(a => a != null && a.AgreementID != null)) { - var agreement = await _yagna.Api.GetAgreement(agreementInfo.AgreementID!); - var activities = await _yagna.Api.GetActivities(agreement!.AgreementID!); - var invoice = invoices.FirstOrDefault(i => i.AgreementId == agreement.AgreementID); - foreach (var activityId in activities) - { - var usage = await _yagna.Api.GetActivityUsage(activityId); - if (usage.CurrentUsage != null) - { - var price = GetPriceFromAgreementAndUsage(agreement, usage.CurrentUsage); - await UpdateJob( - activityId, - invoice, - price != null ? new GolemUsage(price) : null); - } - } + await UpdateJobStatus(agreementInfo.Id); + await UpdateJobUsage(agreementInfo.Id); + + var invoice = invoices.FirstOrDefault(i => i.AgreementId == agreementInfo.Id); + if (invoice != null) + await UpdateJobPayment(invoice); } return _jobs.Values.Where(job => job.Timestamp >= since).Cast().ToList(); @@ -148,7 +141,7 @@ await UpdateJob( public async Task UpdateJob(string activityId, Invoice? invoice, GolemUsage? usage) { var agreementId = await _yagna.Api.GetActivityAgreement(activityId); - await UpdateJobStatus(activityId); + await UpdateJobStatus(agreementId); if (invoice != null) await UpdateJobPayment(invoice); @@ -182,19 +175,31 @@ public async Task UpdateJobPayment(Invoice invoice) return job; } - private async Task UpdateJobStatus(string activityId) + private async Task UpdateJobStatus(string agreementId) { - var activityStatePair = await _yagna.Api.GetState(activityId); - var agreementId = await _yagna.Api.GetActivityAgreement(activityId); - var agreement = await _yagna.Api.GetAgreement(agreementId); var job = await GetOrCreateJob(agreementId); + var agreement = await _yagna.Api.GetAgreement(agreementId); - if (activityStatePair != null) - job.UpdateActivityState(activityStatePair); - - // In case activity state wasn't properly updated by Provider or ExeUnit. + // Agreement state has precedens over individual activity states. if (agreement.State == "Terminated") + { job.Status = JobStatus.Finished; + } + else + { + var activities = await _yagna.Api.GetActivities(agreementId); + foreach (var activity in activities) + { + var activityStatePair = await _yagna.Api.GetState(activity); + + // Assumption: Only single activity is allowed at the same time and rest of + // them will be terminated properly by Reqestor or Provider agent. + // If assumption is not valid, then Job state will change in strange way depending on activities order. + // This won't rather happen in correct cases. In incorrect case we will have incorrect state anyway. + if (activityStatePair.currentState() != StateType.Terminated) + job.UpdateActivityState(activityStatePair); + } + } return job; } From 88afc47052cb696a2afe24719c4fe6d1e7ece79e Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Mon, 15 Apr 2024 20:09:57 +0200 Subject: [PATCH 07/18] Refactor UpdateJob to better handle both usages --- Golem/ActivityLoop.cs | 21 +++++++++++---------- Golem/InvoiceEventsLoop.cs | 3 +-- Golem/Jobs.cs | 12 +++++++++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Golem/ActivityLoop.cs b/Golem/ActivityLoop.cs index 09f609e2..11152cc3 100644 --- a/Golem/ActivityLoop.cs +++ b/Golem/ActivityLoop.cs @@ -50,12 +50,12 @@ public async Task Start( await foreach (var trackingEvent in _yagnaApi.ActivityMonitorStream(token)) { var activities = trackingEvent?.Activities ?? new List(); - + List currentJobs = await UpdateJobs(_jobs, activities); - + var job = SelectCurrentJob(currentJobs); setCurrentJob(job); - if(job == null) + if (job == null) { // Sometimes finished jobs end up in Idle state _jobs.SetAllJobsFinished(); @@ -91,7 +91,7 @@ public async Task Start( if (currentJobs.Count == 0) { _logger.LogDebug("Cleaning current job field"); - + return null; } else if (currentJobs.Count == 1) @@ -133,12 +133,13 @@ public async Task> UpdateJobs(IJobs jobs, List activity { var result = await Task.WhenAll( activityStates - .Select(async d => { - var usage = d.Usage!=null - ? GolemUsage.From(d.Usage) - : null; - return await jobs.UpdateJob(d.Id, null, usage); - } + .Select(async d => + { + var usage = d.Usage != null + ? GolemUsage.From(d.Usage) + : null; + return await jobs.UpdateJobByActivity(d.Id, null, usage); + } ) ); diff --git a/Golem/InvoiceEventsLoop.cs b/Golem/InvoiceEventsLoop.cs index da4ef467..801f34fc 100644 --- a/Golem/InvoiceEventsLoop.cs +++ b/Golem/InvoiceEventsLoop.cs @@ -63,7 +63,6 @@ private async Task UpdatesForInvoice(InvoiceEvent invoiceEvent) var invoice = await _yagnaApi.GetInvoice(invoiceEvent.InvoiceId, _token); _logger.LogDebug("Update Invoice info for Job: {}, status: {}", invoice.AgreementId, invoice.Status); - foreach (var activityId in invoice.ActivityIds) - await _jobs.UpdateJob(activityId, invoice, null); + await _jobs.UpdateJob(invoice.AgreementId, invoice, null); } } diff --git a/Golem/Jobs.cs b/Golem/Jobs.cs index 21ad1258..d5100dc4 100644 --- a/Golem/Jobs.cs +++ b/Golem/Jobs.cs @@ -17,7 +17,8 @@ public interface IJobs { Task GetOrCreateJob(string jobId); void SetAllJobsFinished(); - Task UpdateJob(string activityId, Invoice? invoice, GolemUsage? usage); + Task UpdateJob(string agreementId, Invoice? invoice, GolemUsage? usage); + Task UpdateJobByActivity(string activityId, Invoice? invoice, GolemUsage? usage); } class Jobs : IJobs @@ -88,7 +89,7 @@ public async Task> List(DateTime since) await UpdateJobPayment(invoice); } - return _jobs.Values.Where(job => job.Timestamp >= since).Cast().ToList(); + return _jobs.Values.Where(job => job.Timestamp >= since).OrderByDescending(job => job.Timestamp).Cast().ToList(); } private GolemPrice? GetPriceFromAgreement(YagnaAgreement agreement) @@ -138,9 +139,14 @@ public async Task> List(DateTime since) return null; } - public async Task UpdateJob(string activityId, Invoice? invoice, GolemUsage? usage) + public async Task UpdateJobByActivity(string activityId, Invoice? invoice, GolemUsage? usage) { var agreementId = await _yagna.Api.GetActivityAgreement(activityId); + return await UpdateJob(agreementId, invoice, usage); + } + + public async Task UpdateJob(string agreementId, Invoice? invoice, GolemUsage? usage) + { await UpdateJobStatus(agreementId); if (invoice != null) From c13ffe95fb14d0abece97744e8e6565366192461 Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Mon, 15 Apr 2024 20:21:30 +0200 Subject: [PATCH 08/18] Use GetInvoicePayments instead of listing all Payments and filtering --- Golem/Jobs.cs | 2 +- Golem/Yagna/YagnaApi.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Golem/Jobs.cs b/Golem/Jobs.cs index d5100dc4..6a3fe588 100644 --- a/Golem/Jobs.cs +++ b/Golem/Jobs.cs @@ -164,7 +164,7 @@ public async Task UpdateJobPayment(Invoice invoice) { var job = await GetOrCreateJob(invoice.AgreementId); - var payments = await _yagna.Api.GetPayments(job.Timestamp); + var payments = await _yagna.Api.GetInvoicePayments(invoice.InvoiceId); var paymentsForRecentJob = payments .Where(p => p.AgreementPayments.Exists(ap => ap.AgreementId == invoice.AgreementId) || p.ActivityPayments.Exists(ap => invoice.ActivityIds.Contains(ap.ActivityId))) .ToList(); diff --git a/Golem/Yagna/YagnaApi.cs b/Golem/Yagna/YagnaApi.cs index a57bcab8..d2133857 100644 --- a/Golem/Yagna/YagnaApi.cs +++ b/Golem/Yagna/YagnaApi.cs @@ -214,6 +214,12 @@ public async Task> GetPayments(DateTime? afterTimestamp = null, Ca return await RestGet>(path, args, token); } + public async Task> GetInvoicePayments(string invoiceId, CancellationToken token = default) + { + var path = $"/payment-api/v1/invoices/{invoiceId}/payments"; + return await RestGet>(path, token); + } + public async Task GetInvoice(string id, CancellationToken token = default) { var path = $"/payment-api/v1/invoices/{id}"; From a708a92d289db42a1a7bf53bec9c9ec5388ee45a Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Mon, 15 Apr 2024 21:09:52 +0200 Subject: [PATCH 09/18] Handle partial payments during Job duration --- Golem/InvoiceEventsLoop.cs | 47 ++++++++++++++++++++++++++++++++------ Golem/Jobs.cs | 11 +++++++++ Golem/Yagna/Job.cs | 8 +++++++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Golem/InvoiceEventsLoop.cs b/Golem/InvoiceEventsLoop.cs index 801f34fc..633a2df9 100644 --- a/Golem/InvoiceEventsLoop.cs +++ b/Golem/InvoiceEventsLoop.cs @@ -11,7 +11,6 @@ class InvoiceEventsLoop private readonly CancellationToken _token; private readonly ILogger _logger; private readonly IJobs _jobs; - private DateTime _since = DateTime.MinValue; public InvoiceEventsLoop(YagnaApi yagnaApi, CancellationToken token, ILogger logger, IJobs jobs) @@ -22,26 +21,29 @@ public InvoiceEventsLoop(YagnaApi yagnaApi, CancellationToken token, ILogger log _jobs = jobs; } - public async Task Start() + public Task Start() { - _logger.LogInformation("Starting monitoring invoice events"); + return Task.WhenAll(PaymentsLoop(), InvoiceLoop()); + } - DateTime newReconnect = DateTime.Now; + public async Task InvoiceLoop() + { + _logger.LogInformation("Starting monitoring invoice events"); await Task.Yield(); + DateTime since = DateTime.Now; while (!_token.IsCancellationRequested) { try { - var invoiceEvents = await _yagnaApi.GetInvoiceEvents(_since, _token); + var invoiceEvents = await _yagnaApi.GetInvoiceEvents(since, _token); if (invoiceEvents != null && invoiceEvents.Count > 0) { - _since = invoiceEvents.Max(x => x.EventDate); - foreach (var invoiceEvent in invoiceEvents.OrderBy(e => e.EventDate)) { await UpdatesForInvoice(invoiceEvent); + since = invoiceEvent.EventDate; } } } @@ -58,6 +60,37 @@ public async Task Start() } } + public async Task PaymentsLoop() + { + _logger.LogInformation("Starting monitoring payments"); + + DateTime since = DateTime.Now; + await Task.Yield(); + + while (!_token.IsCancellationRequested) + { + try + { + var payments = await _yagnaApi.GetPayments(since, _token); + foreach (var payment in payments.OrderBy(pay => pay.Timestamp)) + { + await _jobs.UpdatePartialPayment(payment); + since = payment.Timestamp; + } + } + catch (TaskCanceledException) + { + _logger.LogInformation("Payments loop cancelled"); + return; + } + catch (Exception e) + { + _logger.LogError("Error in payments loop: {e}", e.Message); + await Task.Delay(TimeSpan.FromSeconds(5), _token); + } + } + } + private async Task UpdatesForInvoice(InvoiceEvent invoiceEvent) { var invoice = await _yagnaApi.GetInvoice(invoiceEvent.InvoiceId, _token); diff --git a/Golem/Jobs.cs b/Golem/Jobs.cs index 6a3fe588..74ef9601 100644 --- a/Golem/Jobs.cs +++ b/Golem/Jobs.cs @@ -19,6 +19,7 @@ public interface IJobs void SetAllJobsFinished(); Task UpdateJob(string agreementId, Invoice? invoice, GolemUsage? usage); Task UpdateJobByActivity(string activityId, Invoice? invoice, GolemUsage? usage); + Task UpdatePartialPayment(Payment payment); } class Jobs : IJobs @@ -181,6 +182,16 @@ public async Task UpdateJobPayment(Invoice invoice) return job; } + public async Task UpdatePartialPayment(Payment payment) + { + foreach (var activityPayment in payment.ActivityPayments) + { + var agreementId = await this._yagna.Api.GetActivityAgreement(activityPayment.ActivityId); + var job = await GetOrCreateJob(agreementId); + job.PartialPayment(payment); + } + } + private async Task UpdateJobStatus(string agreementId) { var job = await GetOrCreateJob(agreementId); diff --git a/Golem/Yagna/Job.cs b/Golem/Yagna/Job.cs index 7b02e713..f04327b2 100644 --- a/Golem/Yagna/Job.cs +++ b/Golem/Yagna/Job.cs @@ -109,6 +109,14 @@ public void UpdateActivityState(ActivityStatePair activityState) this.Status = ResolveStatus(currentState, nextState); } + public void PartialPayment(Payment payment) + { + if (!PaymentConfirmation.Exists(pay => pay.PaymentId == payment.PaymentId)) + { + PaymentConfirmation.Add(payment); + } + } + private JobStatus ResolveStatus(StateType currentState, StateType? nextState) { switch (currentState) From 3f44a3bc94210fdd578f5b7818640b0ad391b0aa Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Mon, 15 Apr 2024 21:16:48 +0200 Subject: [PATCH 10/18] Fix workaround for Invoice Settled status not set by yagna --- Golem/Jobs.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Golem/Jobs.cs b/Golem/Jobs.cs index 74ef9601..96631dc5 100644 --- a/Golem/Jobs.cs +++ b/Golem/Jobs.cs @@ -172,11 +172,15 @@ public async Task UpdateJobPayment(Invoice invoice) job.PaymentConfirmation = paymentsForRecentJob; job.PaymentStatus = IntoPaymentStatus(invoice.Status); + var confirmedSum = job.PaymentConfirmation.Sum(payment => Convert.ToDecimal(payment.Amount)); + + _logger.LogInformation($"Job: {job.Id}, confirmed sum: {confirmedSum}, job expected reward: {job.CurrentReward}"); + // Workaround for yagna unable to change status to SETTLED when using partial payments if (invoice.Status == InvoiceStatus.ACCEPTED - && job.CurrentReward == job.PaymentConfirmation.Sum(payment => Convert.ToDecimal(payment.Amount))) + && job.CurrentReward == confirmedSum) { - invoice.Status = InvoiceStatus.SETTLED; + job.PaymentStatus = IntoPaymentStatus(InvoiceStatus.SETTLED); } return job; From 108eb28d3138dff39f6e398d8176ecd19489a18e Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Thu, 18 Apr 2024 16:15:53 +0200 Subject: [PATCH 11/18] Configurable payment interval; additions to readme --- ExampleRunner/Program.cs | 7 +++++-- Golem.Tools/App/app.py | 10 ++++++---- Golem.Tools/SampleApp.cs | 5 +++++ Readme.md | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/ExampleRunner/Program.cs b/ExampleRunner/Program.cs index 6031c6c6..259883fb 100644 --- a/ExampleRunner/Program.cs +++ b/ExampleRunner/Program.cs @@ -22,8 +22,9 @@ public class AppArguments [Option('f', "framework", Default = Framework.Automatic, Required = false, HelpText = "Type of AI Framework to run")] public required Framework AiFramework { get; set; } [Option('m', "mainnet", Default = false, Required = false, HelpText = "Enables usage of mainnet")] - public required bool Mainnet { get; set; } - + public required bool Mainnet { get; set; } + [Option('p', "pay-interval", Default = false, Required = false, HelpText = "Interval between partial payments in seconds")] + public UInt32? PaymentInterval { get; set; } } @@ -41,6 +42,8 @@ static void Main(string[] args) GolemRelay.SetEnv(parsed.Relay); var App = new FullExample(workDir, "Requestor", loggerFactory, runtime: parsed.AiFramework.ToString().ToLower(), parsed.Mainnet); + + App.PaymentInterval = parsed.PaymentInterval; var logger = loggerFactory.CreateLogger("Example"); _ = Task.Run(async () => diff --git a/Golem.Tools/App/app.py b/Golem.Tools/App/app.py index e3cb53bf..1f59192d 100644 --- a/Golem.Tools/App/app.py +++ b/Golem.Tools/App/app.py @@ -61,6 +61,7 @@ def build_parser(description: str) -> argparse.ArgumentParser: ) parser.add_argument("--runtime", default="dummy", help="Runtime name, for example `automatic`") parser.add_argument("--descriptor", default=None, help="Path to node descriptor file") + parser.add_argument("--pay-interval", default=180, help="Interval of making partial payments") return parser @@ -131,11 +132,11 @@ class ProviderOnceStrategy(MarketStrategy): """Hires provider only once. """ - def __init__(self): + def __init__(self, pay_interval=180): self.history = set(()) self.acceptable_prop_value_range_overrides = { PROP_DEBIT_NOTE_INTERVAL_SEC: PropValueRange(60, None), - PROP_PAYMENT_TIMEOUT_SEC: PropValueRange(180, None), + PROP_PAYMENT_TIMEOUT_SEC: PropValueRange(int(pay_interval), None), } async def score_offer(self, offer): @@ -199,8 +200,8 @@ def __init__(self, strategy: ProviderOnceStrategy): self.strategy = strategy -async def main(subnet_tag, descriptor, driver=None, network=None, runtime="dummy"): - strategy = ProviderOnceStrategy() +async def main(subnet_tag, descriptor, driver=None, network=None, runtime="dummy", args=None): + strategy = ProviderOnceStrategy(pay_interval=args.pay_interval) async with Golem( budget=4.0, subnet_tag=subnet_tag, @@ -296,6 +297,7 @@ def instances(): driver=args.payment_driver, network=args.payment_network, runtime=args.runtime, + args=args ), log_file=args.log_file, ) diff --git a/Golem.Tools/SampleApp.cs b/Golem.Tools/SampleApp.cs index 1d5dc563..f69f9a53 100644 --- a/Golem.Tools/SampleApp.cs +++ b/Golem.Tools/SampleApp.cs @@ -53,6 +53,7 @@ public class FullExample : IAsyncDisposable, INotifyPropertyChanged private readonly bool _mainnet; private readonly string _runtime; + public UInt32? PaymentInterval { get; set; } private string _message; public string Message @@ -97,6 +98,10 @@ private string ExtraArgs() { args += $" --descriptor {GetNodeDescriptor()}"; } + if (PaymentInterval.HasValue) + { + args += $" --pay-interval {PaymentInterval.Value}"; + } return args; } diff --git a/Readme.md b/Readme.md index ae3701d5..61721090 100644 --- a/Readme.md +++ b/Readme.md @@ -106,6 +106,21 @@ For this reason Payment Status won't be displayed, when the job is finished. To > Current imlementation of `List Jobs` displays only tasks computed during current session of `MockGUI` application. > This is temporary behavior which will be reimplemented according to specification later. +### Partial payments + +Example runner is configured to use partial payments. +You can configure payments interval using command: +``` +dotnet run --project ExampleRunner -- --golem modules --framework Dummy --pay-interval 120 +``` + +Interval controls how often Provider will send payable DebitNotes. +Note that this doesn't mean that you will get payments in these regular intervals. First payment should be done between 120s-240s, but yagna payment driver can batch transactions to optimize gas costs, so later we can expect different intervals than requested. + +`pay-interval` can have lowest value of 120s. Lower value will not be permitted and it is generally dicouraged to set too low value because blockchain has it's inertion. It is hard to ensure that payments are made in time. + +Provider can break Agreement if it doesn't receive payment. Since it is only example application, these case are not handled, but setting high enough interval value (which is understood as payment timeout as well) should solve this problem if it is too anoying. + ## Troublshooting ### Provider doesn't pick up tasks From d0d8ea309714b9b41b4d653744653e51e2bee04f Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Thu, 18 Apr 2024 16:21:15 +0200 Subject: [PATCH 12/18] Use yagna pre-rel-v0.16.0-rc16 --- Golem.Package/Args.cs | 4 ++-- Golem.Tools/GolemPackageBuilder.cs | 4 ++-- MockGUI/readme.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Golem.Package/Args.cs b/Golem.Package/Args.cs index 8e69203d..4e29640b 100644 --- a/Golem.Package/Args.cs +++ b/Golem.Package/Args.cs @@ -5,9 +5,9 @@ public class BuildArgs { [Option('t', "target", Default = "package", Required = false, HelpText = "Directory where binaries will be generated relative to working dir")] public required string Target { get; set; } - [Option('y', "yagna-version", Default = "pre-rel-v0.16.0-ai-rc15", Required = false, HelpText = "Yagna version github tag")] + [Option('y', "yagna-version", Default = "pre-rel-v0.16.0-ai-rc16", Required = false, HelpText = "Yagna version github tag")] public required string GolemVersion { get; set; } - [Option('r', "runtime-version", Default = "v0.2.0", Required = false, HelpText = "Runtime version github tag")] + [Option('r', "runtime-version", Default = "pre-rel-v0.2.1-rc1", Required = false, HelpText = "Runtime version github tag")] public required string RuntimeVersion { get; set; } [Option('c', "dont-clean", Default = false, Required = false, HelpText = "Remove temporary directories")] public required bool DontClean { get; set; } diff --git a/Golem.Tools/GolemPackageBuilder.cs b/Golem.Tools/GolemPackageBuilder.cs index b711c658..3b527e8a 100644 --- a/Golem.Tools/GolemPackageBuilder.cs +++ b/Golem.Tools/GolemPackageBuilder.cs @@ -18,8 +18,8 @@ namespace Golem.Tools { public class PackageBuilder { - public static string CURRENT_GOLEM_VERSION = "pre-rel-v0.16.0-ai-rc15"; - public static string CURRENT_RUNTIME_VERSION = "v0.2.0"; + public static string CURRENT_GOLEM_VERSION = "pre-rel-v0.16.0-ai-rc16"; + public static string CURRENT_RUNTIME_VERSION = "pre-rel-v0.2.1-rc1"; internal static string InitTestDirectory(string name, bool cleanupData = true) { diff --git a/MockGUI/readme.md b/MockGUI/readme.md index f1a1203f..d02cf3f2 100644 --- a/MockGUI/readme.md +++ b/MockGUI/readme.md @@ -39,7 +39,7 @@ dotnet run --project Golem.Package -- download --target modules --version v3.0.0 In case of building artifacts locally you can specify `yagna` and `runtimes` versions: ```sh -dotnet run --project Golem.Package -- build --target modules --yagna-version pre-rel-v0.16.0-ai-rc15 --runtime-version v0.2.0 +dotnet run --project Golem.Package -- build --target modules --yagna-version pre-rel-v0.16.0-ai-rc16 --runtime-version pre-rel-v0.2.1-rc1 ``` ## Running From f955169e85f0e444365c9b0b2d1fc1f1537c30ef Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Thu, 18 Apr 2024 16:31:13 +0200 Subject: [PATCH 13/18] pay-interval default=null --- ExampleRunner/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExampleRunner/Program.cs b/ExampleRunner/Program.cs index 259883fb..07348367 100644 --- a/ExampleRunner/Program.cs +++ b/ExampleRunner/Program.cs @@ -23,7 +23,7 @@ public class AppArguments public required Framework AiFramework { get; set; } [Option('m', "mainnet", Default = false, Required = false, HelpText = "Enables usage of mainnet")] public required bool Mainnet { get; set; } - [Option('p', "pay-interval", Default = false, Required = false, HelpText = "Interval between partial payments in seconds")] + [Option('p', "pay-interval", Default = null, Required = false, HelpText = "Interval between partial payments in seconds")] public UInt32? PaymentInterval { get; set; } } From fe65f8b21f14fa3c2a2094d881ea90cc2ff1bea6 Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Thu, 18 Apr 2024 17:54:51 +0200 Subject: [PATCH 14/18] Remove unnecessary yield --- Golem/InvoiceEventsLoop.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Golem/InvoiceEventsLoop.cs b/Golem/InvoiceEventsLoop.cs index 633a2df9..d642e2bc 100644 --- a/Golem/InvoiceEventsLoop.cs +++ b/Golem/InvoiceEventsLoop.cs @@ -30,9 +30,7 @@ public async Task InvoiceLoop() { _logger.LogInformation("Starting monitoring invoice events"); - await Task.Yield(); DateTime since = DateTime.Now; - while (!_token.IsCancellationRequested) { try @@ -65,8 +63,6 @@ public async Task PaymentsLoop() _logger.LogInformation("Starting monitoring payments"); DateTime since = DateTime.Now; - await Task.Yield(); - while (!_token.IsCancellationRequested) { try From f6fefb068a30c139b2a16c84943d3b295c51fef2 Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Thu, 18 Apr 2024 19:17:24 +0200 Subject: [PATCH 15/18] CultureInvariant ToDecimal conversion --- Golem/Jobs.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Golem/Jobs.cs b/Golem/Jobs.cs index 96631dc5..96dba243 100644 --- a/Golem/Jobs.cs +++ b/Golem/Jobs.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text.Json; using Golem.Model; @@ -172,7 +173,7 @@ public async Task UpdateJobPayment(Invoice invoice) job.PaymentConfirmation = paymentsForRecentJob; job.PaymentStatus = IntoPaymentStatus(invoice.Status); - var confirmedSum = job.PaymentConfirmation.Sum(payment => Convert.ToDecimal(payment.Amount)); + var confirmedSum = job.PaymentConfirmation.Sum(payment => Convert.ToDecimal(payment.Amount, CultureInfo.InvariantCulture)); _logger.LogInformation($"Job: {job.Id}, confirmed sum: {confirmedSum}, job expected reward: {job.CurrentReward}"); From ec57e82b8aae76b243c606d5312cb10baec554b2 Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Fri, 19 Apr 2024 15:23:08 +0200 Subject: [PATCH 16/18] Fix Invoice and Payments loop async Tasks --- Golem/Golem.cs | 2 +- Golem/InvoiceEventsLoop.cs | 21 ++++++++++++++++----- Golem/Yagna/YagnaApi.cs | 4 +--- Golem/Yagna/YagnaService.cs | 6 +++++- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Golem/Golem.cs b/Golem/Golem.cs index 40f37221..d5535d16 100644 --- a/Golem/Golem.cs +++ b/Golem/Golem.cs @@ -373,7 +373,7 @@ private void SetCurrentJob(Job? job) } else { - _logger.LogInformation("Job has not changed."); + _logger.LogDebug("Job has not changed."); } } diff --git a/Golem/InvoiceEventsLoop.cs b/Golem/InvoiceEventsLoop.cs index d642e2bc..89ffc159 100644 --- a/Golem/InvoiceEventsLoop.cs +++ b/Golem/InvoiceEventsLoop.cs @@ -23,7 +23,7 @@ public InvoiceEventsLoop(YagnaApi yagnaApi, CancellationToken token, ILogger log public Task Start() { - return Task.WhenAll(PaymentsLoop(), InvoiceLoop()); + return Task.WhenAll(Task.Run(PaymentsLoop), Task.Run(InvoiceLoop)); } public async Task InvoiceLoop() @@ -31,21 +31,26 @@ public async Task InvoiceLoop() _logger.LogInformation("Starting monitoring invoice events"); DateTime since = DateTime.Now; - while (!_token.IsCancellationRequested) + while (true) { try { + _token.ThrowIfCancellationRequested(); + _logger.LogDebug("Checking for new invoice events since: {}", since); + var invoiceEvents = await _yagnaApi.GetInvoiceEvents(since, _token); if (invoiceEvents != null && invoiceEvents.Count > 0) { foreach (var invoiceEvent in invoiceEvents.OrderBy(e => e.EventDate)) { + _logger.LogDebug("Invoice event for: {}", invoiceEvent.InvoiceId); + await UpdatesForInvoice(invoiceEvent); since = invoiceEvent.EventDate; } } } - catch (TaskCanceledException) + catch (OperationCanceledException) { _logger.LogInformation("Invoice events loop cancelled"); return; @@ -63,18 +68,24 @@ public async Task PaymentsLoop() _logger.LogInformation("Starting monitoring payments"); DateTime since = DateTime.Now; - while (!_token.IsCancellationRequested) + while (true) { try { + + _token.ThrowIfCancellationRequested(); + _logger.LogDebug("Checking for new payments since: {}", since); + var payments = await _yagnaApi.GetPayments(since, _token); foreach (var payment in payments.OrderBy(pay => pay.Timestamp)) { + _logger.LogDebug("New Payment, id: {}", payment.PaymentId); + await _jobs.UpdatePartialPayment(payment); since = payment.Timestamp; } } - catch (TaskCanceledException) + catch (OperationCanceledException) { _logger.LogInformation("Payments loop cancelled"); return; diff --git a/Golem/Yagna/YagnaApi.cs b/Golem/Yagna/YagnaApi.cs index d2133857..27b117a4 100644 --- a/Golem/Yagna/YagnaApi.cs +++ b/Golem/Yagna/YagnaApi.cs @@ -246,9 +246,7 @@ public async Task> GetInvoiceEvents(DateTime since, Cancellat {"X-Provider-Events", string.Join(',', _monitorEventTypes)} }; - var result = await RestGet>("/payment-api/v1/invoiceEvents", args, headers, token); - - return result; + return await RestGet>("/payment-api/v1/invoiceEvents", args, headers, token); } public void CancelPendingRequests() diff --git a/Golem/Yagna/YagnaService.cs b/Golem/Yagna/YagnaService.cs index d4a9b1d6..506027f6 100644 --- a/Golem/Yagna/YagnaService.cs +++ b/Golem/Yagna/YagnaService.cs @@ -223,6 +223,8 @@ public async Task Stop(int stopTimeoutMs = 30_000) public async Task WaitForIdentityAsync(CancellationToken cancellationToken = default) { + _logger.LogDebug("Waiting for yagna to start... Checking /me endpoint."); + //yagna is starting and /me won't work until all services are running for (int tries = 0; tries < 200; ++tries) { @@ -239,6 +241,8 @@ public async Task Stop(int stopTimeoutMs = 30_000) try { MeInfo meInfo = await Api.Me(cancellationToken); + + _logger.LogDebug("Yagna started; REST API is available."); return meInfo.Identity; } catch (Exception) @@ -261,7 +265,7 @@ public Task StartActivityLoop(CancellationToken token, Action setCurrentJo /// TODO: Reconsider API of this function. public Task StartInvoiceEventsLoop(CancellationToken token, IJobs jobs) { - return new InvoiceEventsLoop(Api, token, _logger, jobs).Start(); + return Task.Run(async () => { await new InvoiceEventsLoop(Api, token, _logger, jobs).Start(); }); } } } From 1d0077a70a4f1515a67c86c7a2381064302fa1f3 Mon Sep 17 00:00:00 2001 From: nieznanysprawiciel Date: Fri, 19 Apr 2024 16:24:04 +0200 Subject: [PATCH 17/18] Update MockGUI/GolemModel.cs Co-authored-by: pwalski <4924911+pwalski@users.noreply.github.com> --- MockGUI/GolemModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MockGUI/GolemModel.cs b/MockGUI/GolemModel.cs index e6679d9d..914ca92c 100644 --- a/MockGUI/GolemModel.cs +++ b/MockGUI/GolemModel.cs @@ -207,6 +207,7 @@ public async void OnListJobs() } catch (Exception) { + _logger.LogWarning(e, "Listing jobs failure"); jobs = new List(); } From 5f8f549f33c69fd809bc61e0e1d20b7e2242d21b Mon Sep 17 00:00:00 2001 From: "nieznany.sprawiciel" Date: Fri, 19 Apr 2024 16:30:27 +0200 Subject: [PATCH 18/18] Fix exception in ListJobs caller --- MockGUI/GolemModel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MockGUI/GolemModel.cs b/MockGUI/GolemModel.cs index 914ca92c..d87a4470 100644 --- a/MockGUI/GolemModel.cs +++ b/MockGUI/GolemModel.cs @@ -36,12 +36,12 @@ public ObservableCollection JobsHistory public ObservableCollection ApplicationEvents { get - { + { return _applicationEvents; } set { - _applicationEvents = value; + _applicationEvents = value; OnPropertyChanged(); } } @@ -205,7 +205,7 @@ public async void OnListJobs() _logger.LogInformation("Listing jobs since: " + since); jobs = await this.Golem.ListJobs(since); } - catch (Exception) + catch (Exception e) { _logger.LogWarning(e, "Listing jobs failure"); jobs = new List();