From dcd312b119c7f960c662a41db77e322858e5a5ff Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Tue, 19 May 2020 16:03:38 +0000 Subject: [PATCH] Backup --- .../GlobalOpsSettingsController.cs | 133 ++++++++++++++++++ server/AyaNova/biz/GlobalOpsSettingsBiz.cs | 104 ++++++++++++++ server/AyaNova/generator/CoreJobBackup.cs | 122 ++++++++++++++++ server/AyaNova/models/AyContext.cs | 1 + server/AyaNova/models/GlobalOpsSettings.cs | 22 +++ .../AyaNova/util/ServerGlobalOpsSettings.cs | 47 +++++++ 6 files changed, 429 insertions(+) create mode 100644 server/AyaNova/Controllers/GlobalOpsSettingsController.cs create mode 100644 server/AyaNova/biz/GlobalOpsSettingsBiz.cs create mode 100644 server/AyaNova/generator/CoreJobBackup.cs create mode 100644 server/AyaNova/models/GlobalOpsSettings.cs create mode 100644 server/AyaNova/util/ServerGlobalOpsSettings.cs diff --git a/server/AyaNova/Controllers/GlobalOpsSettingsController.cs b/server/AyaNova/Controllers/GlobalOpsSettingsController.cs new file mode 100644 index 00000000..144a1ab0 --- /dev/null +++ b/server/AyaNova/Controllers/GlobalOpsSettingsController.cs @@ -0,0 +1,133 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; +using AyaNova.Models; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Biz; +using System.Threading.Tasks; + +namespace AyaNova.Api.Controllers +{ + + [ApiController] + [ApiVersion("8.0")] + [Route("api/v{version:apiVersion}/global-biz-setting")] + [Produces("application/json")] + [Authorize] + public class GlobalOpsSettingsController : ControllerBase + { + private readonly AyContext ct; + private readonly ILogger log; + private readonly ApiServerState serverState; + + + + /// + /// ctor + /// + /// + /// + /// + public GlobalOpsSettingsController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) + { + ct = dbcontext; + log = logger; + serverState = apiServerState; + } + + /// + /// Get GlobalOpsSettings + /// + /// Global settings object + [HttpGet] + public async Task GetGlobalOpsSettings() + { + if (serverState.IsClosed) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + //Instantiate the business object handler + GlobalOpsSettingsBiz biz = GlobalOpsSettingsBiz.GetBiz(ct, HttpContext); + + if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + var o = await biz.GetAsync(); + if (o == null) + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + return Ok(ApiOkResponse.Response(o)); + } + + + /// + /// POST (replace) Global biz settings + /// + /// + /// nothing + [HttpPost] + public async Task ReplaceGlobalOpsSettings([FromBody] GlobalOpsSettings global) + { + if (serverState.IsClosed) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + //Instantiate the business object handler + GlobalOpsSettingsBiz biz = GlobalOpsSettingsBiz.GetBiz(ct, HttpContext); + + if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + try + { + if (!await biz.ReplaceAsync(global)) + return BadRequest(new ApiErrorResponse(biz.Errors)); + } + catch (DbUpdateConcurrencyException) + { + return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT)); + } + return NoContent(); + } + + /// + /// Get Client app relevant GlobalOpsSettings + /// + /// Global settings object + [HttpGet("client")] + public ActionResult GetClientGlobalOpsSettings() + { + if (serverState.IsClosed) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + //Instantiate the business object handler + // GlobalOpsSettingsBiz biz = GlobalOpsSettingsBiz.GetBiz(ct, HttpContext); + + //this route is available to any logged in user as it contains a subset of limited options relevant to any logged in user + // if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) + // return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + // var o = await biz.GetAsync(); + // if (o == null) + // return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + + //new object with only relevant items in it + var ret = new + { + SearchCaseSensitiveOnly = AyaNova.Util.ServerGlobalOpsSettings.SearchCaseSensitiveOnly + }; + + return Ok(ApiOkResponse.Response(ret)); + } + + }//eoc +}//ens \ No newline at end of file diff --git a/server/AyaNova/biz/GlobalOpsSettingsBiz.cs b/server/AyaNova/biz/GlobalOpsSettingsBiz.cs new file mode 100644 index 00000000..f0b15458 --- /dev/null +++ b/server/AyaNova/biz/GlobalOpsSettingsBiz.cs @@ -0,0 +1,104 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using AyaNova.Util; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Models; + +namespace AyaNova.Biz +{ + + internal class GlobalOpsSettingsBiz : BizObject + { + + internal GlobalOpsSettingsBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) + { + ct = dbcontext; + UserId = currentUserId; + UserTranslationId = userTranslationId; + CurrentUserRoles = UserRoles; + BizType = AyaType.Global; + } + + internal static GlobalOpsSettingsBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) + { + if (httpContext != null) + return new GlobalOpsSettingsBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); + else + return new GlobalOpsSettingsBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull); + } + + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + /// GET + + //Get one + internal async Task GetAsync(bool logTheGetEvent = true) + { + //first try to fetch from db + var ret = await ct.GlobalOpsSettings.SingleOrDefaultAsync(m => m.Id == 1); + if (logTheGetEvent && ret != null) + { + //Log + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 1, BizType, AyaEvent.Retrieved), ct); + } + + //not in db then get the default + if (ret == null) + { + throw new System.Exception("GlobalOpsSettingsBiz::GetAsync -> Global settings object not found in database!!"); + } + return ret; + } + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //UPDATE + // + + //put + internal async Task ReplaceAsync(GlobalOpsSettings inObj) + { + var dbObj = await ct.GlobalOpsSettings.FirstOrDefaultAsync(m => m.Id == 1); + if (dbObj == null) + throw new System.Exception("GlobalOpsSettingsBiz::ReplaceAsync -> Global settings object not found in database!!"); + CopyObject.Copy(inObj, dbObj, "Id"); + + ct.Entry(dbObj).OriginalValues["Concurrency"] = inObj.Concurrency; + + Validate(dbObj); + if (HasErrors) + return false; + await ct.SaveChangesAsync(); + //Log modification and save context + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 1, BizType, AyaEvent.Modified), ct); + //Update the static copy for the server + ServerGlobalOpsSettings.Initialize(dbObj); + return true; + } + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //VALIDATION + // + + //Can save or update? + private void Validate(GlobalOpsSettings inObj) + { + + //currently nothing to validate + } + + + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons + diff --git a/server/AyaNova/generator/CoreJobBackup.cs b/server/AyaNova/generator/CoreJobBackup.cs new file mode 100644 index 00000000..9c9e1cf6 --- /dev/null +++ b/server/AyaNova/generator/CoreJobBackup.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using AyaNova.Models; + + +namespace AyaNova.Biz +{ + + + /// + /// Backup + /// + /// + internal static class CoreJobBackup + { + private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("CoreJobBackup"); + private static DateTime lastSweep = DateTime.MinValue; + private static TimeSpan SWEEP_EVERY_INTERVAL = new TimeSpan(0, 30, 0); + private static TimeSpan SUCCEEDED_JOBS_DELETE_AFTER_THIS_TIMESPAN = new TimeSpan(24, 0, 0);//24 hours + 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 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) + + //////////////////////////////////////////////////////////////////////////////////////////////// + // + // + public static async Task DoWorkAsync(AyContext ct) + { + + + //This will get triggered roughly every minute, but we don't want to sweep that frequently + if (DateTime.UtcNow - lastSweep < SWEEP_EVERY_INTERVAL) + return; + + log.LogTrace("Backup starting"); + + //lock the server + //serverState.SetSystemLock(msg); + //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); + + lastSweep = 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 + { + await JobsBiz.DeleteJobAndLogAsync(j.GId, ct); + } + catch (Exception ex) + { + log.LogError(ex, "sweepAsync exception calling JobsBiz.DeleteJobAndLogAsync"); + //for now just throw it but this needs to be removed when logging added and better handling + throw (ex); + } + } + } + + + /// + /// 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", ct); + 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, ct); + } + } + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons + diff --git a/server/AyaNova/models/AyContext.cs b/server/AyaNova/models/AyContext.cs index 32b9c59d..0dc59561 100644 --- a/server/AyaNova/models/AyContext.cs +++ b/server/AyaNova/models/AyContext.cs @@ -9,6 +9,7 @@ namespace AyaNova.Models public virtual DbSet UserOptions { get; set; } public virtual DbSet Widget { get; set; } public virtual DbSet GlobalBizSettings { get; set; } + public virtual DbSet GlobalOpsSettings { get; set; } public virtual DbSet Event { get; set; } public virtual DbSet SearchDictionary { get; set; } public virtual DbSet SearchKey { get; set; } diff --git a/server/AyaNova/models/GlobalOpsSettings.cs b/server/AyaNova/models/GlobalOpsSettings.cs new file mode 100644 index 00000000..db9eff16 --- /dev/null +++ b/server/AyaNova/models/GlobalOpsSettings.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; +namespace AyaNova.Models +{ + + public class GlobalOpsSettings + { + public long Id { get; set; }//this is always 1 as there is only ever one single global Ops object + public uint Concurrency { get; set; } + + //Global settings + //Picklist and other searches override the normal case insensitive value + //this is precautionarily added for non latinate languages where it could be an issue + public bool SearchCaseSensitiveOnly {get;set;} + + public GlobalOpsSettings() + { + Id=1;//always 1 + SearchCaseSensitiveOnly = false; + } + } +} diff --git a/server/AyaNova/util/ServerGlobalOpsSettings.cs b/server/AyaNova/util/ServerGlobalOpsSettings.cs new file mode 100644 index 00000000..33c55d34 --- /dev/null +++ b/server/AyaNova/util/ServerGlobalOpsSettings.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using AyaNova.Models; + +namespace AyaNova.Util +{ + + /// + /// Contains static mirror copy in memory of global settings values that are set from DB during boot + /// and accessible to Biz admin user (equivalent of v7's global object) + /// used by many areas of the biz logic and processing too often to fetch on every request + /// set at boot and on any update to the db global biz settings record + /// + internal static class ServerGlobalOpsSettings + { + + internal static bool SearchCaseSensitiveOnly { get; set; } + + /// + /// Populate and / or create the settings + /// + internal static void Initialize(GlobalBizSettings global, AyContext ct = null) + { + + if (global == null) + { + //fetch or create as not provided (meaning this was called from Startup.cs) + global = ct.GlobalBizSettings.FirstOrDefault(z => z.Id == 1); + if (global == null) + { + global = new GlobalBizSettings(); + ct.GlobalBizSettings.Add(global); + ct.SaveChanges(); + } + } + + //We have the object, now copy the static values here + SearchCaseSensitiveOnly = global.SearchCaseSensitiveOnly; + } + + + + }//eoc +}//eons \ No newline at end of file