Compare commits
No commits in common. "main" and "v1.5.1" have entirely different histories.
15 changed files with 50 additions and 130 deletions
|
|
@ -14,7 +14,7 @@
|
|||
<Product>SQLInsight Pro</Product>
|
||||
<Copyright>Copyright (c) SEOSoft</Copyright>
|
||||
<!-- Release version; set with -p:Version=… when publishing. Update checks compare against it. -->
|
||||
<Version>1.5.2</Version>
|
||||
<Version>1.5.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('Tests'))">
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
-- venter på næste gentagelse, men dets seneste kørsel i SQLInsights egen historik (insight.AxBatchRuns) endte med
|
||||
-- fejl inden for grænsen.
|
||||
-- STATUS oversættes af versionens adapter: AX 2009+ 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet,
|
||||
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 samme rækkefølge (1 og 4 kontrolleret mod kundedata 2026-10-03):
|
||||
-- 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet. Højst 25 fund pr. database.
|
||||
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 (antaget, ikke verificeret mod kundedata)
|
||||
-- 0 Venter, 1 Udfører, 2 Fejl, 3 Afsluttet, 4 Tilbageholdt. Højst 25 fund pr. database.
|
||||
--
|
||||
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
|
||||
-- SQLInsight:DynamicsAx:BatchFailedLookbackHours = 24: fejl inden for så mange timer rapporteres.
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@
|
|||
-- BATCHJOBHISTORY i AX 2009+, ellers de kørsler, SQLInsight selv ser blive afsluttet. Hænger jobbet (SQLI-AX-004),
|
||||
-- gives kun det fund.
|
||||
-- STATUS oversættes af versionens adapter: AX 2009+ 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet,
|
||||
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 samme rækkefølge (1 og 4 kontrolleret mod kundedata 2026-10-03):
|
||||
-- 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet. Højst 25 fund pr. database.
|
||||
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 (antaget, ikke verificeret mod kundedata)
|
||||
-- 0 Venter, 1 Udfører, 2 Fejl, 3 Afsluttet, 4 Tilbageholdt. Højst 25 fund pr. database.
|
||||
--
|
||||
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
|
||||
-- SQLInsight:DynamicsAx:BatchLongRunningFactor = 2.0: kørt mere end så mange gange jobbets 90 %-fraktil
|
||||
|
|
|
|||
|
|
@ -19,14 +19,12 @@
|
|||
-- sommertidsregler, der gjaldt på datoen. AX' tomme dato (1900-01-01) er "ingen". Job med status Tilbageholdt er
|
||||
-- ikke forsinkede. De mest forsinkede først.
|
||||
-- STATUS oversættes af versionens adapter: AX 2009+ 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet,
|
||||
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 samme rækkefølge: 0 Tilbageholdt, 1 Venter,
|
||||
-- 2 Udfører, 3 Fejl, 4 Afsluttet (1 og 4 kontrolleret mod kundedata 2026-10-03). Højst 25 fund pr. database.
|
||||
-- Job planlagt for mere end BatchAbandonedDays siden regnes som opgivne rækker og meldes ikke.
|
||||
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 (antaget, ikke verificeret mod kundedata)
|
||||
-- 0 Venter, 1 Udfører, 2 Fejl, 3 Afsluttet, 4 Tilbageholdt. Højst 25 fund pr. database.
|
||||
--
|
||||
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
|
||||
-- SQLInsight:DynamicsAx:BatchLateMinutes = 30: forsinket mere end så mange minutter: Medium
|
||||
-- SQLInsight:DynamicsAx:BatchNeverStartedHours = 24: forsinket mere end så mange timer: High, "aldrig startet"
|
||||
-- SQLInsight:DynamicsAx:BatchAbandonedDays = 30: planlagt for mere end så mange dage siden: opgivet, ingen fund
|
||||
--
|
||||
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
|
||||
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@
|
|||
-- efter sin egen historik (insight.AxBatchRuns, mindst BatchHistoryMinimumRuns kørsler) normalt kører så længe
|
||||
-- (kørt <= faktor x 90 %-fraktil).
|
||||
-- STATUS oversættes af versionens adapter: AX 2009+ 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet,
|
||||
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 samme rækkefølge (1 og 4 kontrolleret mod kundedata 2026-10-03):
|
||||
-- 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet. Højst 25 fund pr. database.
|
||||
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 (antaget, ikke verificeret mod kundedata)
|
||||
-- 0 Venter, 1 Udfører, 2 Fejl, 3 Afsluttet, 4 Tilbageholdt. Højst 25 fund pr. database.
|
||||
--
|
||||
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
|
||||
-- SQLInsight:DynamicsAx:BatchStuckHours = 6: kørt mindst så mange timer
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Den præcise SQL for hver regel står i [`SQL Scripts/`](../SQL%20Scripts/) (fx
|
|||
|---|---|---|
|
||||
| SQLI-AX-001 | Batchjob er endt med fejl (nu, eller seneste kørsel af et gentaget job) | `BatchFailedLookbackHours` = 24 |
|
||||
| SQLI-AX-002 | Batchjob kører længere end normalt, målt mod jobbets egne seneste kørsler | `BatchLongRunningFactor` = 2 × 90 %-fraktilen, mindst `BatchLongRunningMinimumMinutes` = 10, efter `BatchHistoryMinimumRuns` = 5 kørsler |
|
||||
| SQLI-AX-003 | Batchjob er ikke startet til tiden; efter et døgn "aldrig startet". Job planlagt for mere end `BatchAbandonedDays` siden regnes som opgivne og meldes ikke | `BatchLateMinutes` = 30, `BatchNeverStartedHours` = 24, `BatchAbandonedDays` = 30 |
|
||||
| SQLI-AX-003 | Batchjob er ikke startet til tiden; efter et døgn "aldrig startet" | `BatchLateMinutes` = 30, `BatchNeverStartedHours` = 24 |
|
||||
| SQLI-AX-004 | Batchjob hænger i Udfører | `BatchStuckHours` = 6 (medmindre jobbet normalt kører så længe) |
|
||||
| SQLI-AX-005 | Nummerserie er ved at løbe tør, nu eller efter prognosen | `NumberSequenceWarnPercent` = 80, `NumberSequenceCriticalPercent` = 95, `ForecastWarnDays` = 90, `ForecastCriticalDays` = 30 |
|
||||
| SQLI-AX-006 | Nummerseriens format har færre cifre end Højeste kræver | – |
|
||||
|
|
@ -75,7 +75,7 @@ Planlagt start er `BATCHJOB.ORIGSTARTDATETIME` (UTC) i AX 2009 og senere, og `BA
|
|||
|
||||
Hvert jobs historik er SQLInsights egen (`insight.AxBatchRuns`, gemt `EventDays` = 90 dage): i AX 2009 og senere fra `BATCHJOBHISTORY`, læst efter RECID; Axapta 3.0 og 4.0 gemmer ingen historik, så SQLInsight registrerer selv de kørsler, den ser blive afsluttet (kørselstiden måles fra første gang, den så jobbet køre, og korte kørsler, der starter og slutter mellem to målinger, kommer ikke med). Et job i Axapta 3.0 er klasse + firma + batchgruppe, fordi AX 3.0 laver en ny række for hver gentagelse.
|
||||
|
||||
**`STATUS` i Axapta 3.0** følger samme rækkefølge som AX 4.0 og senere: 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet. Venter (1) og Afsluttet (4) er kontrolleret mod en kundes data med [`dynamics-ax/verificer-ax30-batchstatus.sql`](dynamics-ax/verificer-ax30-batchstatus.sql); 0, 2 og 3 fandtes ikke i dataene. Siden *Batchjob* viser stadig de rå værdier. AX 2009 og senere følger Microsofts dokumenterede `BatchStatus` (0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet, 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret).
|
||||
**`STATUS` i Axapta 3.0 er ikke verificeret mod kundedata.** SQLInsight antager 0 Venter, 1 Udfører, 2 Fejl, 3 Afsluttet, 4 Tilbageholdt. Siden *Batchjob* viser de rå værdier, der findes, så de kan sammenlignes med batchlisten i AX. AX 2009 og senere følger Microsofts dokumenterede `BatchStatus` (0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet, 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret).
|
||||
|
||||
### Nummerserier og RecId
|
||||
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
-- ===================================================================================================================
|
||||
-- Verificér STATUS-værdierne i BATCH for Axapta 3.0
|
||||
-- ===================================================================================================================
|
||||
-- Formål: SQLInsight bruger for AX 3.0: 0 = Tilbageholdt, 1 = Venter, 2 = Udfører, 3 = Fejl, 4 = Afsluttet
|
||||
-- (1 og 4 kontrolleret hos Cascorp 2026-10-03).
|
||||
-- Tekst-labels for enums ligger ikke i databasen, så scriptet viser værdierne sammen med de
|
||||
-- kolonner, der afslører betydningen (sluttid, fejlkode, planlagt start), og eksempler, som kan
|
||||
-- slås op i AX-klienten (Basis > Forespørgsler > Batchliste).
|
||||
-- Read-only: Kun SELECT. Ingen temp-tabeller, intet skrives. Kort lock timeout, så batchkørsler ikke blokeres.
|
||||
-- Kør: I SSMS i AX-databasen (fx AXDBTEST på KGA-SQL-01\AX30). Skemaet er bmssa hos Cascorp;
|
||||
-- hedder det dbo hos jer, så erstat "bmssa." med "dbo.".
|
||||
-- ===================================================================================================================
|
||||
|
||||
SET NOCOUNT ON;
|
||||
SET LOCK_TIMEOUT 2000;
|
||||
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
|
||||
|
||||
-- 1. Fordeling: hvor mange job har hver STATUS, og hvad kendetegner dem?
|
||||
SELECT
|
||||
b.STATUS,
|
||||
COUNT(*) AS antal,
|
||||
SUM(CASE WHEN b.ENDDATE > '19000101' THEN 1 ELSE 0 END) AS med_sluttid,
|
||||
SUM(CASE WHEN b.EXCEPTIONCODE <> 0 THEN 1 ELSE 0 END) AS med_fejlkode,
|
||||
SUM(CASE WHEN DATEADD(second, b.STARTTIME, b.STARTDATE) > GETDATE()
|
||||
THEN 1 ELSE 0 END) AS start_i_fremtiden,
|
||||
SUM(CASE WHEN b.SESSIONIDX <> 0 THEN 1 ELSE 0 END) AS med_session,
|
||||
MIN(DATEADD(second, b.STARTTIME, b.STARTDATE)) AS tidligste_start,
|
||||
MAX(DATEADD(second, b.STARTTIME, b.STARTDATE)) AS seneste_start
|
||||
FROM bmssa.BATCH AS b
|
||||
GROUP BY b.STATUS
|
||||
ORDER BY b.STATUS;
|
||||
|
||||
-- 2. Op til 5 eksempler pr. STATUS (nyeste først) til at slå op i AX-klientens batchliste.
|
||||
SELECT x.STATUS, x.COMPANY, x.GROUPID, x.CLASSNUM, x.JOURNALID, x.EXECUTEDBY,
|
||||
x.planlagt_start, x.slut, x.EXCEPTIONCODE, x.SESSIONIDX, x.RECID
|
||||
FROM (
|
||||
SELECT b.STATUS, b.COMPANY, b.GROUPID, b.CLASSNUM, b.JOURNALID, b.EXECUTEDBY,
|
||||
DATEADD(second, b.STARTTIME, b.STARTDATE) AS planlagt_start,
|
||||
CASE WHEN b.ENDDATE > '19000101'
|
||||
THEN DATEADD(second, b.ENDTIME, b.ENDDATE) END AS slut,
|
||||
b.EXCEPTIONCODE, b.SESSIONIDX, b.RECID,
|
||||
ROW_NUMBER() OVER (PARTITION BY b.STATUS ORDER BY b.STARTDATE DESC, b.STARTTIME DESC) AS rn
|
||||
FROM bmssa.BATCH AS b
|
||||
) AS x
|
||||
WHERE x.rn <= 5
|
||||
ORDER BY x.STATUS, x.planlagt_start DESC;
|
||||
|
||||
-- 3. Hvilken feltype har STATUS ifølge AX' egen dictionary? (FIELDTYPE 4 = enum)
|
||||
SELECT d.TABLEID, d.FIELDID, d.NAME, d.SQLNAME, d.FIELDTYPE
|
||||
FROM bmssa.SQLDICTIONARY AS d
|
||||
WHERE d.SQLNAME = 'STATUS'
|
||||
AND d.TABLEID = (SELECT TOP (1) t.TABLEID FROM bmssa.SQLDICTIONARY AS t
|
||||
WHERE t.FIELDID = 0 AND t.SQLNAME = 'BATCH');
|
||||
|
|
@ -13,12 +13,6 @@ Begge kommer fra et **opdateringsfeed**: en https-adresse eller en mappe/et fild
|
|||
|
||||
Læs dette, før en ny version installeres. Repository-skemaet opgraderes af agenten ved første start.
|
||||
|
||||
### 1.5.2: Axapta 3.0-batchstatus rettet
|
||||
|
||||
- Batchjobbenes `STATUS` i Axapta 3.0 tolkes nu som i AX 4.0 og senere: 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet (kontrolleret mod en kundes data). Før 1.5.2 blev afsluttede job vist som tilbageholdte og ventende som kørende, så batchfund og -sider for AX 3.0 var forkerte. Fund fra den gamle tolkning lukkes ved næste måling.
|
||||
- SQLI-AX-003 melder ikke længere ventende job, der var planlagt for mere end `SQLInsight:DynamicsAx:BatchAbandonedDays` (30) dage siden: de er opgivne rækker, ikke en aktuel fejl.
|
||||
- Ingen skemaændringer.
|
||||
|
||||
### 1.5: Windows-servere og syntetiske tjek
|
||||
|
||||
- **Ny menu – Servere:** *Windows-servere* (CPU, hukommelse, diske med prognose for, hvornår de er fulde, tjenester, hændelseslog og planlagte opgaver) og *Syntetiske tjek* (TCP-port, HTTP(S) med certifikatets udløb, SQL-login og forespørgsel, DNS og Dynamics AX-logon), og det samme i API v1 under `/api/v1/windows-servers`, `/api/v1/synthetic-checks` og `/api/v1/server-findings` (ændringer kræver scope `config:write`, som i 1.3). Nye regler `SQLI-WIN-001` … `SQLI-WIN-013` og `SQLI-SYN-001` … `SQLI-SYN-003`. Se [windows-servere.md](windows-servere.md) og [syntetiske-tjek.md](syntetiske-tjek.md).
|
||||
|
|
@ -164,18 +158,18 @@ Kører web i IIS, angives `"AppOffline": true` i stedet for `Service`. Så lægg
|
|||
dotnet run --project tools/SQLInsight.LicenseTool -- rulepack --version 2026.10.14 --out rulepack-2026.10.14.json
|
||||
|
||||
# MSI til installationer lavet med MSI'en (bygges på Linux)
|
||||
installer/build-msi.sh 1.5.2
|
||||
installer/build-msi.sh 1.5.1
|
||||
|
||||
# Programpakker: én zip pr. platform med mapperne Agent/ og Web/
|
||||
# Samme opsætning som MSI'en (indbygget .NET), så en opdatering ikke kræver en runtime på serveren
|
||||
dotnet publish src/SQLInsight.Agent -c Release -r win-x64 --self-contained true -p:Version=1.5.2 -o pkg/win-x64/Agent
|
||||
dotnet publish src/SQLInsight.Web -c Release -r win-x64 --self-contained true -p:Version=1.5.2 -o pkg/win-x64/Web
|
||||
dotnet publish src/SQLInsight.Agent -c Release -r win-x64 --self-contained true -p:Version=1.5.1 -o pkg/win-x64/Agent
|
||||
dotnet publish src/SQLInsight.Web -c Release -r win-x64 --self-contained true -p:Version=1.5.1 -o pkg/win-x64/Web
|
||||
|
||||
# Signér kanalens manifest (det, der ikke angives, overtages fra det eksisterende manifest)
|
||||
dotnet run --project tools/SQLInsight.LicenseTool -- publish --key sqlinsight-k20260929.private.pem --kid k20260929 \
|
||||
--feed feed --channel stable --rulepack rulepack-2026.10.14.json \
|
||||
--release 1.5.2 --asset win-x64=sqlinsight-1.5.2-win-x64.zip --asset win-x64-msi=dist/SQLInsight-Pro-1.5.2-x64.msi \
|
||||
--asset linux-x64=sqlinsight-1.5.2-linux-x64.zip \
|
||||
--release 1.5.1 --asset win-x64=sqlinsight-1.5.1-win-x64.zip --asset win-x64-msi=dist/SQLInsight-Pro-1.5.1-x64.msi \
|
||||
--asset linux-x64=sqlinsight-1.5.1-linux-x64.zip \
|
||||
--notes https://git.seosoft.dk/seo/sqlinsight-pro/releases
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -137,9 +137,6 @@ public sealed class DynamicsAxOptions
|
|||
/// <summary>Late by this many hours: in practice never started (High).</summary>
|
||||
public int BatchNeverStartedHours { get; set; } = 24;
|
||||
|
||||
/// <summary>A waiting job planned more than this many days ago is abandoned and not reported as late.</summary>
|
||||
public int BatchAbandonedDays { get; set; } = 30;
|
||||
|
||||
/// <summary>A batch job executing this many hours is stuck (SQLI-AX-004).</summary>
|
||||
public int BatchStuckHours { get; set; } = 6;
|
||||
|
||||
|
|
@ -192,7 +189,6 @@ public sealed class DynamicsAxOptions
|
|||
{
|
||||
BatchLateMinutes = Math.Max(1, BatchLateMinutes),
|
||||
BatchNeverStartedHours = Math.Max(1, BatchNeverStartedHours),
|
||||
BatchAbandonedDays = Math.Max(2, BatchAbandonedDays),
|
||||
BatchStuckHours = Math.Max(1, BatchStuckHours),
|
||||
BatchLongRunningFactor = Math.Max(1.1, BatchLongRunningFactor),
|
||||
BatchLongRunningMinimumMinutes = Math.Max(1, BatchLongRunningMinimumMinutes),
|
||||
|
|
|
|||
|
|
@ -514,8 +514,8 @@ public abstract class AxSchemaAdapter
|
|||
/// Axapta 2.5/3.0. Verified against the real AX 3.0 schema from a customer (docs/dynamics-ax/schema-ax30.tsv): tables in
|
||||
/// the bmssa schema, 32-bit RECID, BATCH with STARTDATE (datetime, local) + STARTTIME (int, seconds since midnight,
|
||||
/// local), NUMBERSEQUENCETABLE with NEXTREC/HIGHEST/FORMAT/BLOCKED/INUSE per DATAAREAID, SYSTEMSEQUENCES with
|
||||
/// NEXTVAL/MAXVAL per TABID and DATAAREAID. The batch STATUS values Venter (1) and Afsluttet (4) are checked against
|
||||
/// customer data; the UI still shows the raw values found.
|
||||
/// NEXTVAL/MAXVAL per TABID and DATAAREAID. The batch STATUS values are NOT verified against customer data (the schema
|
||||
/// file has no data): the UI shows the raw values found so the mapping can be checked.
|
||||
/// </summary>
|
||||
public sealed class Ax30Adapter : AxSchemaAdapter
|
||||
{
|
||||
|
|
@ -537,12 +537,19 @@ public sealed class Ax30Adapter : AxSchemaAdapter
|
|||
};
|
||||
|
||||
/// <summary>
|
||||
/// The Axapta 3.0 BatchStatus enum: 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet - the same order as AX 4.0
|
||||
/// and the first five values of AX 2009+. Checked against a customer's BATCH data (docs/dynamics-ax/verificer-ax30-batchstatus.sql,
|
||||
/// 2026-10-03): every STATUS 4 row has an end time and no exception code (Afsluttet), and STATUS 1 rows have no end time
|
||||
/// and planned starts long passed (Venter). 0, 2 and 3 were not present in the data and follow the enum order.
|
||||
/// The Axapta 3.0 BatchStatus enum as SQLInsight assumes it: 0 Venter, 1 Udfører, 2 Fejl, 3 Afsluttet, 4 Tilbageholdt.
|
||||
/// NOT verified against customer data; check with SELECT STATUS, COUNT(*) FROM bmssa.BATCH GROUP BY STATUS and the
|
||||
/// batch list in AX. The web UI shows the values found.
|
||||
/// </summary>
|
||||
public override AxBatchStatus BatchStatus(int status) => status is >= 0 and <= 4 ? JobStatus(status) : AxBatchStatus.Unknown;
|
||||
public override AxBatchStatus BatchStatus(int status) => status switch
|
||||
{
|
||||
0 => AxBatchStatus.Waiting,
|
||||
1 => AxBatchStatus.Executing,
|
||||
2 => AxBatchStatus.Error,
|
||||
3 => AxBatchStatus.Finished,
|
||||
4 => AxBatchStatus.Hold,
|
||||
_ => AxBatchStatus.Unknown,
|
||||
};
|
||||
|
||||
public override string JobKey(AxBatchJobRow row)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ public sealed record AxThresholds
|
|||
/// <summary>Late by more than this many hours: it has in practice never started (High).</summary>
|
||||
public int BatchNeverStartedHours { get; init; } = 24;
|
||||
|
||||
/// <summary>A waiting job planned more than this many days ago is abandoned, not late: SQLI-AX-003 skips it.</summary>
|
||||
public int BatchAbandonedDays { get; init; } = 30;
|
||||
|
||||
/// <summary>A batch job executing longer than this many hours is stuck (SQLI-AX-004).</summary>
|
||||
public int BatchStuckHours { get; init; } = 6;
|
||||
|
||||
|
|
|
|||
|
|
@ -189,10 +189,7 @@ public sealed class AxBatchLateRule : DynamicsAxBatchRule
|
|||
|
||||
protected override IEnumerable<Finding> Check(AxBatchDatabase database, AxThresholds thresholds, DateTimeOffset now)
|
||||
{
|
||||
// Jobs planned long ago and never picked up are abandoned rows (e.g. a batch group no server serves), not a live problem.
|
||||
var late = database.Jobs
|
||||
.Where(j => j.Late(now) is { } l && l >= TimeSpan.FromMinutes(thresholds.BatchLateMinutes) && l < TimeSpan.FromDays(thresholds.BatchAbandonedDays))
|
||||
.OrderByDescending(j => j.Late(now)).ToList();
|
||||
var late = database.Jobs.Where(j => j.Late(now) is { } l && l >= TimeSpan.FromMinutes(thresholds.BatchLateMinutes)).OrderByDescending(j => j.Late(now)).ToList();
|
||||
foreach (var job in late)
|
||||
{
|
||||
var behind = job.Late(now)!.Value;
|
||||
|
|
|
|||
|
|
@ -212,13 +212,8 @@ public class AxInterpretationTests
|
|||
[Fact]
|
||||
public void Batch_status_enums_per_version()
|
||||
{
|
||||
// Axapta 3.0, checked against customer data: 1 = waiting (no end time), 4 = finished (end time, no exception code).
|
||||
Assert.Equal(AxBatchStatus.Hold, AxSchemaAdapter.Ax30.BatchStatus(0));
|
||||
Assert.Equal(AxBatchStatus.Waiting, AxSchemaAdapter.Ax30.BatchStatus(1));
|
||||
Assert.Equal(AxBatchStatus.Executing, AxSchemaAdapter.Ax30.BatchStatus(2));
|
||||
Assert.Equal(AxBatchStatus.Error, AxSchemaAdapter.Ax30.BatchStatus(3));
|
||||
Assert.Equal(AxBatchStatus.Finished, AxSchemaAdapter.Ax30.BatchStatus(4));
|
||||
Assert.Equal(AxBatchStatus.Unknown, AxSchemaAdapter.Ax30.BatchStatus(5));
|
||||
Assert.Equal(AxBatchStatus.Executing, AxSchemaAdapter.Ax30.BatchStatus(1));
|
||||
Assert.Equal(AxBatchStatus.Error, AxSchemaAdapter.Ax30.BatchStatus(2));
|
||||
Assert.Equal(AxBatchStatus.Waiting, AxSchemaAdapter.Ax2009.BatchStatus(1));
|
||||
Assert.Equal(AxBatchStatus.Executing, AxSchemaAdapter.Ax2012.BatchStatus(2));
|
||||
Assert.Equal(AxBatchStatus.Canceled, AxSchemaAdapter.Ax2012.BatchStatus(8));
|
||||
|
|
@ -229,7 +224,7 @@ public class AxInterpretationTests
|
|||
[Fact]
|
||||
public void Ax30_jobs_are_keyed_by_class_company_and_group_and_late_jobs_are_measured_from_the_planned_start()
|
||||
{
|
||||
var row = new AxBatchJobRow(17, 1234, null, "dat", "NAT", 1, new DateTime(2026, 10, 2), 9 * 3600, null, null, null, null, -120);
|
||||
var row = new AxBatchJobRow(17, 1234, null, "dat", "NAT", 0, new DateTime(2026, 10, 2), 9 * 3600, null, null, null, null, -120);
|
||||
|
||||
var job = AxSchemaAdapter.Ax30.Interpret(row, new AxBatchHistory(), "AX30", null);
|
||||
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ public class DynamicsAxTests(SqlServerFixture sql) : IAsyncLifetime
|
|||
DECLARE @late datetime, @running datetime, @failed datetime;
|
||||
SET @late = DATEADD(hour, -2, GETDATE()); SET @running = DATEADD(minute, -30, GETDATE()); SET @failed = DATEADD(hour, -1, GETDATE());
|
||||
INSERT bmssa.BATCH (RECID, CLASSNUM, COMPANY, GROUPID, STATUS, STARTDATE, STARTTIME, ENDDATE, ENDTIME)
|
||||
VALUES (1, 101, 'dat', 'NAT', 1, CONVERT(date, @late), DATEDIFF(second, CONVERT(date, @late), @late), '19000101', 0),
|
||||
(2, 102, 'dat', 'NAT', 3, CONVERT(date, @failed), DATEDIFF(second, CONVERT(date, @failed), @failed), CONVERT(date, @failed), DATEDIFF(second, CONVERT(date, @failed), @failed) + 60),
|
||||
(3, 103, 'dat', '', 2, CONVERT(date, @running), DATEDIFF(second, CONVERT(date, @running), @running), '19000101', 0),
|
||||
(4, 104, 'dmo', '', 4, CONVERT(date, @failed), 0, CONVERT(date, @failed), 600);
|
||||
VALUES (1, 101, 'dat', 'NAT', 0, CONVERT(date, @late), DATEDIFF(second, CONVERT(date, @late), @late), '19000101', 0),
|
||||
(2, 102, 'dat', 'NAT', 2, CONVERT(date, @failed), DATEDIFF(second, CONVERT(date, @failed), @failed), CONVERT(date, @failed), DATEDIFF(second, CONVERT(date, @failed), @failed) + 60),
|
||||
(3, 103, 'dat', '', 1, CONVERT(date, @running), DATEDIFF(second, CONVERT(date, @running), @running), '19000101', 0),
|
||||
(4, 104, 'dmo', '', 3, CONVERT(date, @failed), 0, CONVERT(date, @failed), 600);
|
||||
INSERT bmssa.NUMBERSEQUENCETABLE (RECID, NUMBERSEQUENCE, TXT, LOWEST, HIGHEST, NEXTREC, FORMAT, BLOCKED, CONTINUOUS, CYCLIC, MANUAL, INUSE,
|
||||
LATESTCLEANDATE, LATESTCLEANTIME, CLEANINTERVAL, DATAAREAID)
|
||||
VALUES (10, 'Debitor', 'Debitorer', 1, 99999, 97000, 'DEB-#####', 0, 0, 0, 0, 1, '19000101', 0, 0, 'dat'),
|
||||
|
|
|
|||
|
|
@ -79,15 +79,15 @@ public class DynamicsAxRuleTests
|
|||
[Fact]
|
||||
public void Ax001_a_batch_job_in_error_is_high_and_an_old_error_is_not_reported()
|
||||
{
|
||||
var failed = Job30(Batch30(10, 3, Utc.AddHours(-3)));
|
||||
var old = Job30(Batch30(11, 3, Utc.AddDays(-3), classNumber: 99));
|
||||
var failed = Job30(Batch30(10, 2, Utc.AddHours(-3)));
|
||||
var old = Job30(Batch30(11, 2, Utc.AddDays(-3), classNumber: 99));
|
||||
|
||||
var finding = Assert.Single(Run(new AxBatchFailedRule(), With(Batch(failed, old))).Findings);
|
||||
|
||||
Assert.Equal(Severity.High, finding.Severity);
|
||||
Assert.Equal("database: AX30 · batch: 4711|dat|NAT", finding.Subject);
|
||||
Assert.Contains("Fejl (STATUS = 3)", finding.Evidence.Single(e => e.Name == "Status i AX").Value, StringComparison.Ordinal);
|
||||
Assert.Equal(RuleOutcome.Passed, Run(new AxBatchFailedRule(), With(Batch(Job30(Batch30(12, 1, Utc.AddMinutes(10)))))).Outcome);
|
||||
Assert.Contains("Fejl (STATUS = 2)", finding.Evidence.Single(e => e.Name == "Status i AX").Value, StringComparison.Ordinal);
|
||||
Assert.Equal(RuleOutcome.Passed, Run(new AxBatchFailedRule(), With(Batch(Job30(Batch30(12, 0, Utc.AddMinutes(10)))))).Outcome);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -114,7 +114,7 @@ public class DynamicsAxRuleTests
|
|||
[InlineData(100, Severity.High)]
|
||||
public void Ax002_a_job_running_much_longer_than_its_own_history(int runningMinutes, Severity? expected)
|
||||
{
|
||||
var row = Batch30(20, 2, Utc.AddHours(-5));
|
||||
var row = Batch30(20, 1, Utc.AddHours(-5));
|
||||
var job = Job30(row, History("4711|dat|NAT", 10, TimeSpan.FromMinutes(20)), firstSeen: Utc.AddMinutes(-runningMinutes));
|
||||
|
||||
var result = Run(new AxBatchLongRunningRule(), With(Batch(job)));
|
||||
|
|
@ -132,7 +132,7 @@ public class DynamicsAxRuleTests
|
|||
[InlineData(60 * 30, Severity.High)]
|
||||
public void Ax003_a_waiting_job_past_its_planned_start_is_late_and_after_a_day_never_started(int minutesLate, Severity? expected)
|
||||
{
|
||||
var job = Job30(Batch30(30, 1, Utc.AddMinutes(-minutesLate)));
|
||||
var job = Job30(Batch30(30, 0, Utc.AddMinutes(-minutesLate)));
|
||||
|
||||
var finding = Run(new AxBatchLateRule(), With(Batch(job))).Findings.SingleOrDefault();
|
||||
|
||||
|
|
@ -144,21 +144,10 @@ public class DynamicsAxRuleTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ax003_a_job_waiting_for_years_is_abandoned_not_late()
|
||||
{
|
||||
// Seen at a customer: 25 waiting AX 3.0 jobs with an empty batch group, planned 2007-2018.
|
||||
var abandoned = Job30(Batch30(32, 1, Utc.AddDays(-Thresholds.BatchAbandonedDays - 1)));
|
||||
var recent = Job30(Batch30(33, 1, Utc.AddDays(-Thresholds.BatchAbandonedDays + 1)));
|
||||
|
||||
Assert.Equal(RuleOutcome.Passed, Run(new AxBatchLateRule(), With(Batch(abandoned))).Outcome);
|
||||
Assert.Single(Run(new AxBatchLateRule(), With(Batch(recent))).Findings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ax003_held_jobs_are_not_late()
|
||||
{
|
||||
var held = Job30(Batch30(31, 0, Utc.AddDays(-2)));
|
||||
var held = Job30(Batch30(31, 4, Utc.AddDays(-2)));
|
||||
|
||||
Assert.Equal(AxBatchStatus.Hold, held.Status);
|
||||
Assert.Equal(RuleOutcome.Passed, Run(new AxBatchLateRule(), With(Batch(held))).Outcome);
|
||||
|
|
@ -167,7 +156,7 @@ public class DynamicsAxRuleTests
|
|||
[Fact]
|
||||
public void Ax004_a_job_executing_for_hours_is_stuck_unless_it_normally_runs_that_long()
|
||||
{
|
||||
var row = Batch30(40, 2, Utc.AddHours(-9));
|
||||
var row = Batch30(40, 1, Utc.AddHours(-9));
|
||||
var stuck = Job30(row, firstSeen: Utc.AddHours(-7));
|
||||
var normallyLong = Job30(row, History("4711|dat|NAT", 10, TimeSpan.FromHours(6)), Utc.AddHours(-7));
|
||||
|
||||
|
|
@ -325,8 +314,8 @@ public class DynamicsAxRuleTests
|
|||
// AX 3.0 keeps a BATCH row per recurrence: the failed run and the next waiting one share the job key.
|
||||
var history = new AxBatchHistory();
|
||||
history.Add("AX30", new AxBatchRun("4711|dat|NAT", "R1", "Lagerafslutning", "dat", Utc.AddHours(-3), Utc.AddHours(-2), AxBatchStatus.Error));
|
||||
var failed = Job30(Batch30(50, 3, Utc.AddHours(-3)), history) with { EndedUtc = Utc.AddHours(-2) };
|
||||
var waiting = Job30(Batch30(51, 1, Utc.AddHours(1)), history);
|
||||
var failed = Job30(Batch30(50, 2, Utc.AddHours(-3)), history) with { EndedUtc = Utc.AddHours(-2) };
|
||||
var waiting = Job30(Batch30(51, 0, Utc.AddHours(1)), history);
|
||||
|
||||
var finding = Assert.Single(Run(new AxBatchFailedRule(), With(Batch(failed, waiting))).Findings);
|
||||
|
||||
|
|
@ -337,7 +326,7 @@ public class DynamicsAxRuleTests
|
|||
[Fact]
|
||||
public void Ax004_one_long_earlier_run_does_not_hide_a_stuck_job_and_ax002_leaves_a_stuck_job_to_ax004()
|
||||
{
|
||||
var row = Batch30(60, 2, Utc.AddHours(-9));
|
||||
var row = Batch30(60, 1, Utc.AddHours(-9));
|
||||
var oneLongRun = Job30(row, History("4711|dat|NAT", 1, TimeSpan.FromHours(10)), Utc.AddHours(-7));
|
||||
Assert.Single(Run(new AxBatchStuckRule(), With(Batch(oneLongRun))).Findings);
|
||||
|
||||
|
|
@ -349,7 +338,7 @@ public class DynamicsAxRuleTests
|
|||
[Fact]
|
||||
public void An_executing_job_without_any_start_time_is_skipped_not_an_error()
|
||||
{
|
||||
var unknown = Job30(Batch30(70, 2, Utc.AddHours(-9)), History("4711|dat|NAT", 10, TimeSpan.FromMinutes(20)));
|
||||
var unknown = Job30(Batch30(70, 1, Utc.AddHours(-9)), History("4711|dat|NAT", 10, TimeSpan.FromMinutes(20)));
|
||||
Assert.Null(unknown.Running(Now));
|
||||
|
||||
Assert.Equal(RuleOutcome.Passed, Run(new AxBatchStuckRule(), With(Batch(unknown))).Outcome);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue