diff --git a/server/AyaNova/Controllers/LicenseController.cs b/server/AyaNova/Controllers/LicenseController.cs index 06974bc1..e7eeef70 100644 --- a/server/AyaNova/Controllers/LicenseController.cs +++ b/server/AyaNova/Controllers/LicenseController.cs @@ -72,20 +72,6 @@ namespace AyaNova.Api.Controllers - // /// - // /// Get Trial status of license - // /// - // /// True if a trial license - // [AllowAnonymous] - // [HttpGet("trial")] - // public ActionResult GetTrialFlag() - // { - // //note: this route is called by the client as the first action so it also acts like a ping to see if the server is up as well - // if (serverState.IsClosed) - // return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - // return Ok(ApiOkResponse.Response(AyaNova.Core.License.ActiveKey.TrialLicense)); - // } - /// @@ -121,9 +107,12 @@ namespace AyaNova.Api.Controllers //main thing is to not log anything but OK response if (ret == "ok") { - //Log + //Eventlog await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserIdFromContext.Id(HttpContext.Items), 0, AyaType.License, AyaEvent.LicenseFetch), ct); - + } + else + { + log.LogError($"LicenseController::FetchLicense - failed: {ret}"); } //any response here is OK, there's not necessarily an error, just no key or some mundane problem not technical but biz so let diff --git a/server/AyaNova/Startup.cs b/server/AyaNova/Startup.cs index da1488a4..0f1e0af1 100644 --- a/server/AyaNova/Startup.cs +++ b/server/AyaNova/Startup.cs @@ -570,7 +570,7 @@ namespace AyaNova if (ServerBootConfig.AYANOVA_SERVER_TEST_MODE) { _newLog.LogInformation($"Server test mode seeding, level is {ServerBootConfig.AYANOVA_SERVER_TEST_MODE_SEEDLEVEL}, tz offset is {ServerBootConfig.AYANOVA_SERVER_TEST_MODE_TZ_OFFSET}"); - AyaNova.Core.License.FetchKeyAsync(apiServerState, dbContext, _newLog).Wait(); + AyaNova.Core.License.FetchKeyAsync(apiServerState, dbContext, _newLog, true).Wait(); //NOTE: For unit testing make sure the time zone is same as tester to ensure list filter by date tests will work because server is on same page as user in terms of time var seed = new Util.Seeder(); seed.SeedDatabaseAsync(Seeder.Level.StringToSeedLevel(ServerBootConfig.AYANOVA_SERVER_TEST_MODE_SEEDLEVEL), ServerBootConfig.AYANOVA_SERVER_TEST_MODE_TZ_OFFSET).Wait(); diff --git a/server/AyaNova/biz/JobsBiz.cs b/server/AyaNova/biz/JobsBiz.cs index f11258b2..b455d24a 100644 --- a/server/AyaNova/biz/JobsBiz.cs +++ b/server/AyaNova/biz/JobsBiz.cs @@ -155,9 +155,9 @@ namespace AyaNova.Biz ActivelyProcessing = true; try { - //Sweep jobs table - //System.Diagnostics.Debug.WriteLine($"JobsBiz processing sweeper"); - await CoreJobSweeper.DoSweepAsync();//run exclusively + //## Critical internal jobs + await CoreJobSweeper.DoSweepAsync(); + await CoreJobLicense.DoWorkAsync(); //BIZOBJECT DYNAMIC JOBS //get a list of exclusive jobs that are due to happen diff --git a/server/AyaNova/generator/CoreJobLicense.cs b/server/AyaNova/generator/CoreJobLicense.cs index 3b9cb98b..beb42c92 100644 --- a/server/AyaNova/generator/CoreJobLicense.cs +++ b/server/AyaNova/generator/CoreJobLicense.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using AyaNova.Models; +using AyaNova.Core; +using AyaNova.Util; namespace AyaNova.Biz { @@ -16,141 +18,74 @@ namespace AyaNova.Biz internal static class CoreJobLicense { private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("CoreJobLicense"); - private static DateTime lastCheck = DateTime.MinValue; - - //define timespans for various existing license scenarios - //120 days after a new license purchase it should check for revoke from chargebacks - //Trial should check regularly for purchase - //revoked should do what? - //rental licenses need to check more often than perpetual -//slow and fast scenarios maybe? Just two timespans, fast for likely purchaser and slow for ongoing rental maint etc - - - private static TimeSpan SWEEP_EVERY_INTERVAL = new TimeSpan(0, 30, 0); - private static TimeSpan SUCCEEDED_JOBS_DELETE_AFTER_THIS_TIMESPAN = new TimeSpan(14, 0, 0, 0);//14 days - private static TimeSpan FAILED_JOBS_DELETE_AFTER_THIS_TIMESPAN = new TimeSpan(14, 0, 0, 0);//14 days (gives people time to notice and look into it) - private static TimeSpan INTERNAL_JOBS_LOGS_DELETE_AFTER_THIS_TIMESPAN = new TimeSpan(14, 0, 0, 0);//14 days - - private static TimeSpan RUNNING_JOBS_BECOME_FAILED_AFTER_THIS_TIMESPAN = new TimeSpan(24, 0, 0);//24 hours (time running jobs are allowed to sit in "running" state before considered failed) + private static DateTime _lastCheck = DateTime.MinValue; + + private static TimeSpan FAST_TRACK = new TimeSpan(0, 0, 30, 0); + private static TimeSpan SLOW_TRACK = new TimeSpan(24, 0, 0); //////////////////////////////////////////////////////////////////////////////////////////////// - // DoSweep + // DoWork // - public static async Task DoSweepAsync() - { - //This will get triggered roughly every minute, but we don't want to sweep that frequently - if (DateTime.UtcNow - lastCheck < SWEEP_EVERY_INTERVAL) + public static async Task DoWorkAsync() + { + log.LogTrace("Job starting"); + var tsSinceLastCheck = DateTime.UtcNow - _lastCheck; + //which track are we on? + /* + NONE = 0,//fast track + ActiveTrial = 1,//slow track + ExpiredTrial = 2,//fast track + ActivePurchased = 3,//slow track + ExpiredPurchased = 4,//fast track + Revoked = 5//slow track + */ + TimeSpan tsCheckFrequency; + switch (AyaNova.Core.License.ActiveKey.Status) + { + case AyaNova.Core.License.AyaNovaLicenseKey.LicenseStatus.NONE: + case AyaNova.Core.License.AyaNovaLicenseKey.LicenseStatus.ExpiredTrial: + case AyaNova.Core.License.AyaNovaLicenseKey.LicenseStatus.ExpiredPurchased: + tsCheckFrequency = FAST_TRACK; + break; + default: + tsCheckFrequency = SLOW_TRACK; + break; + } + log.LogTrace($"Check frequency{tsCheckFrequency}"); + if (tsSinceLastCheck < tsCheckFrequency) + { + log.LogTrace($"Not yet"); return; + } - log.LogTrace("Sweep starting"); using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext) { - //SWEEP SUCCESSFUL JOBS - //calculate cutoff to delete - DateTime dtDeleteCutoff = DateTime.UtcNow - SUCCEEDED_JOBS_DELETE_AFTER_THIS_TIMESPAN; - await sweepAsync(ct, dtDeleteCutoff, JobStatus.Completed); - - //SWEEP FAILED JOBS - //calculate cutoff to delete - dtDeleteCutoff = DateTime.UtcNow - FAILED_JOBS_DELETE_AFTER_THIS_TIMESPAN; - await sweepAsync(ct, dtDeleteCutoff, JobStatus.Failed); - - //KILL STUCK JOBS - //calculate cutoff to delete - DateTime dtRunningDeadline = DateTime.UtcNow - RUNNING_JOBS_BECOME_FAILED_AFTER_THIS_TIMESPAN; - await killStuckJobsAsync(ct, dtRunningDeadline); - - //SWEEP INTERNAL JOB LOG - //calculate cutoff to delete - dtDeleteCutoff = DateTime.UtcNow - INTERNAL_JOBS_LOGS_DELETE_AFTER_THIS_TIMESPAN; - await SweepInternalJobsLogsAsync(ct, dtDeleteCutoff); - - } - lastCheck = DateTime.UtcNow; - } - - - private static async Task sweepAsync(AyContext ct, DateTime dtDeleteCutoff, JobStatus jobStatus) - { - //Get the deleteable succeeded jobs list - var jobs = await ct.OpsJob - .AsNoTracking() - .Where(z => z.Created < dtDeleteCutoff && z.JobStatus == jobStatus) - .OrderBy(z => z.Created) - .ToListAsync(); - - log.LogTrace($"SweepAsync processing: cutoff={dtDeleteCutoff.ToString()}, for {jobs.Count.ToString()} jobs of status {jobStatus.ToString()}"); - - foreach (OpsJob j in jobs) - { - try + try//allow exception to bubble up but using try block to ensure the lastcheck is set properly regardless so this doesn't spin out of control { - - await JobsBiz.RemoveJobAndLogsAsync(j.GId); + var ServerState = (AyaNova.Api.ControllerHelpers.ApiServerState)ServiceProviderProvider.Provider.GetService(typeof(AyaNova.Api.ControllerHelpers.ApiServerState)); + + var ret = await AyaNova.Core.License.FetchKeyAsync(ServerState, ct, log, true); + //most often the result will be "notfound" but in future might be other results + if (ret == "ok") + { + //Eventlog + await EventLogProcessor.LogEventToDatabaseAsync(new Event(1, 0, AyaType.License, AyaEvent.LicenseFetch, "FromCoreJob"), ct); + } + if(ret!="notfound") + { + //I'm thinking do not log internally failed except as trace event as this would fill up log file + //instead if they have a license issue they can do manual fetch and then that will log so they can see error + //(also it will return an error to the client) + log.LogTrace($"FetchLicense - failed: {ret}"); + } } - catch (Exception ex) + finally { - log.LogError(ex, "sweepAsync exception calling JobsBiz.RemoveJobAndLogsAsync"); - //for now just throw it but this needs to be removed when logging added and better handling - throw (ex); + _lastCheck = DateTime.UtcNow; } } } - - - /// - /// Kill jobs that have been stuck in "running" state for too long - /// - private static async Task killStuckJobsAsync(AyContext ct, DateTime dtRunningDeadline) - { - //Get the deleteable succeeded jobs list - var jobs = await ct.OpsJob - .AsNoTracking() - .Where(z => z.Created < dtRunningDeadline && z.JobStatus == JobStatus.Running) - .OrderBy(z => z.Created) - .ToListAsync(); - - log.LogTrace($"killStuckJobsAsync processing: cutoff={dtRunningDeadline.ToString()}, for {jobs.Count.ToString()} jobs of status {JobStatus.Running.ToString()}"); - - foreach (OpsJob j in jobs) - { - //OPSMETRIC - await JobsBiz.LogJobAsync(j.GId, "Job took too long to run - setting to failed"); - log.LogError($"Job found job stuck in running status and set to failed: deadline={dtRunningDeadline.ToString()}, jobId={j.GId.ToString()}, jobname={j.Name}, jobtype={j.JobType.ToString()}, jobObjectType={j.ObjectType.ToString()}, jobObjectId={j.ObjectId.ToString()}"); - await JobsBiz.UpdateJobStatusAsync(j.GId, JobStatus.Failed); - } - } - - - private static async Task SweepInternalJobsLogsAsync(AyContext ct, DateTime dtDeleteCutoff) - { - //Get the deleteable list (this is for reporting, could easily just do it in one go) - var logs = await ct.OpsJobLog - .AsNoTracking() - .Where(z => z.Created < dtDeleteCutoff) - .OrderBy(z => z.Created) - .ToListAsync(); - - log.LogTrace($"SweepInternalJobsLogsAsync processing: cutoff={dtDeleteCutoff.ToString()}, for {logs.Count.ToString()} log entries"); - - foreach (OpsJobLog l in logs) - { - try - { - await ct.Database.ExecuteSqlInterpolatedAsync($"delete from aopsjoblog where gid = {l.GId}"); - } - catch (Exception ex) - { - log.LogError(ex, "SweepInternalJobsLogsAsync exception removed old log entries"); - throw (ex); - } - } - } - ///////////////////////////////////////////////////////////////////// - }//eoc - - }//eons diff --git a/server/AyaNova/util/License.cs b/server/AyaNova/util/License.cs index 037d0e91..de4d12c2 100644 --- a/server/AyaNova/util/License.cs +++ b/server/AyaNova/util/License.cs @@ -86,12 +86,12 @@ namespace AyaNova.Core public enum LicenseStatus { - NONE = 0, - ActiveTrial = 1, - ExpiredTrial = 2, - ActivePurchased = 3, - ExpiredPurchased = 4, - Revoked = 5 + NONE = 0,//fast track + ActiveTrial = 1,//slow track + ExpiredTrial = 2,//fast track + ActivePurchased = 3,//slow track + ExpiredPurchased = 4,//fast track + Revoked = 5//slow track } public AyaNovaLicenseKey() @@ -174,7 +174,6 @@ namespace AyaNova.Core return LicenseStatus.ActivePurchased; if (!TrialLicense && LicenseExpired) return LicenseStatus.ExpiredPurchased; - throw new System.Exception("License::Status - unable to determine license status"); } } @@ -484,9 +483,13 @@ namespace AyaNova.Core /// Fetch a key, validate it and install it in the db then initialize with it /// /// Result string - internal static async Task FetchKeyAsync(AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ct, ILogger log, bool calledFromInternalJob = true) + internal static async Task FetchKeyAsync(AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ct, ILogger log, bool calledFromInternalJob) { - log.LogInformation($"Fetching license for DBID {LicenseDbId.ToString()}"); + if (calledFromInternalJob) + log.LogTrace($"Fetching license for DBID {LicenseDbId.ToString()} (called by job)"); + else + log.LogInformation($"Fetching license for DBID {LicenseDbId.ToString()}"); + string sUrl = $"{LICENSE_SERVER_URL}rvf/{LicenseDbId.ToString()}"; try { @@ -504,11 +507,8 @@ namespace AyaNova.Core if (ParsedKey != null) { await InstallAsync(keyText, ParsedKey, apiServerState, ct, log); - log.LogInformation($"Installed new license key {ParsedKey.Id}"); //Log the license info so it's on the record log.LogInformation($"License - new key installed [{AyaNova.Core.License.LicenseInfoLogFormat}]"); - - return "ok"; } return $"E1020 - Error fetching license key: No key was returned";