This commit is contained in:
2020-05-19 16:03:38 +00:00
parent 54812b5469
commit dcd312b119
6 changed files with 429 additions and 0 deletions

View File

@@ -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<GlobalOpsSettingsController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public GlobalOpsSettingsController(AyContext dbcontext, ILogger<GlobalOpsSettingsController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get GlobalOpsSettings
/// </summary>
/// <returns>Global settings object</returns>
[HttpGet]
public async Task<IActionResult> 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));
}
/// <summary>
/// POST (replace) Global biz settings
/// </summary>
/// <param name="global"></param>
/// <returns>nothing</returns>
[HttpPost]
public async Task<IActionResult> 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();
}
/// <summary>
/// Get Client app relevant GlobalOpsSettings
/// </summary>
/// <returns>Global settings object</returns>
[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

View File

@@ -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<GlobalOpsSettings> 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<bool> 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

View File

@@ -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
{
/// <summary>
/// Backup
///
/// </summary>
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);
}
}
}
/// <summary>
/// Kill jobs that have been stuck in "running" state for too long
/// </summary>
/// <param name="ct"></param>
/// <param name="dtRunningDeadline"></param>
/// <returns></returns>
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

View File

@@ -9,6 +9,7 @@ namespace AyaNova.Models
public virtual DbSet<UserOptions> UserOptions { get; set; }
public virtual DbSet<Widget> Widget { get; set; }
public virtual DbSet<GlobalBizSettings> GlobalBizSettings { get; set; }
public virtual DbSet<GlobalOpsSettings> GlobalOpsSettings { get; set; }
public virtual DbSet<Event> Event { get; set; }
public virtual DbSet<SearchDictionary> SearchDictionary { get; set; }
public virtual DbSet<SearchKey> SearchKey { get; set; }

View File

@@ -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;
}
}
}

View File

@@ -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
{
/// <summary>
/// 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
/// </summary>
internal static class ServerGlobalOpsSettings
{
internal static bool SearchCaseSensitiveOnly { get; set; }
/// <summary>
/// Populate and / or create the settings
/// </summary>
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