Skip to content

Commit e4455d3

Browse files
Merge branch 'dev' of https://github.com/KelvinTegelaar/CIPP-API into dev
2 parents bfb0c17 + 64f30df commit e4455d3

9 files changed

Lines changed: 97 additions & 40 deletions

File tree

Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,32 @@ function Get-CIPPAlertQuotaUsed {
88
[Parameter(Mandatory = $false)]
99
[Alias('input')]
1010
$InputValue,
11+
[Parameter(Mandatory)]
1112
$TenantFilter
1213
)
1314

15+
$Threshold = if ($InputValue.QuotaUsedQuota) { [int]$InputValue.QuotaUsedQuota } else { 90 }
16+
$ExcludedRaw = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text ([string]$InputValue.QuotaUsedExcludedMailboxes)
17+
$Excluded = @($ExcludedRaw -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ })
18+
1419
try {
15-
$AlertData = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application/json" -tenantid $TenantFilter
20+
$AlertData = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application/json&`$top=999" -tenantid $TenantFilter
1621
} catch {
22+
$ErrorMessage = Get-CippException -Exception $_
23+
Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Mailbox quota Alert: Unable to get mailbox usage: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
1724
return
1825
}
26+
1927
$OverQuota = $AlertData | ForEach-Object {
20-
if ([string]::IsNullOrEmpty($_.StorageUsedInBytes) -or [string]::IsNullOrEmpty($_.prohibitSendReceiveQuotaInBytes) -or $_.StorageUsedInBytes -eq 0 -or $_.prohibitSendReceiveQuotaInBytes -eq 0) { return }
21-
try {
22-
$PercentLeft = [math]::round(($_.storageUsedInBytes / $_.prohibitSendReceiveQuotaInBytes) * 100)
23-
} catch { $PercentLeft = 100 }
24-
try {
25-
if ([int]$InputValue -gt 0) {
26-
$Value = [int]$InputValue
27-
} else {
28-
$Value = 90
29-
}
30-
} catch {
31-
$Value = 90
32-
}
33-
if ($PercentLeft -gt $Value) {
28+
if (!$_.StorageUsedInBytes -or !$_.prohibitSendReceiveQuotaInBytes) { return }
29+
if ($Excluded -contains $_.userPrincipalName.ToLower()) { return }
30+
$UsagePercent = [math]::Round(($_.storageUsedInBytes / $_.prohibitSendReceiveQuotaInBytes) * 100)
31+
if ($UsagePercent -gt $Threshold) {
3432
[PSCustomObject]@{
35-
Message = "$($_.userPrincipalName): Mailbox is more than $($value)% full. Mailbox is $PercentLeft% full"
33+
Message = "$($_.userPrincipalName): Mailbox is more than $($Threshold)% full. Mailbox is $UsagePercent% full"
3634
Owner = $_.userPrincipalName
3735
RecipientType = $_.recipientType
38-
UsagePercent = $PercentLeft
36+
UsagePercent = $UsagePercent
3937
StorageUsedInBytes = $_.storageUsedInBytes
4038
ProhibitSendReceiveQuotaInBytes = $_.prohibitSendReceiveQuotaInBytes
4139
Tenant = $TenantFilter

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ using namespace Microsoft.Azure.Functions.PowerShellWorker
33
function New-CippCoreRequest {
44
<#
55
.SYNOPSIS
6-
Main entrypoint for all HTTP triggered functions in CIPP
6+
Main entrypoint for all HTTP triggered functions in CIPP, this must live in the CIPPCore module
77
.DESCRIPTION
88
This function is the main entry point for all HTTP triggered functions in CIPP. It routes requests to the appropriate function based on the CIPPEndpoint parameter in the request.
99
.FUNCTIONALITY

Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ function Start-CIPPDBTestsRun {
3838
}
3939

4040
try {
41+
try { [CIPP.TestDataCache]::Clear() } catch { Write-Information "TestDataCache clear skipped: $($_.Exception.Message)" }
42+
4143
$AllTenantsList = if ($TenantFilter -eq 'allTenants') {
4244
$DbCounts = Get-CIPPDbItem -CountsOnly -TenantFilter 'allTenants'
4345
$TenantsWithData = $DbCounts | Where-Object { (($_.DataCount ?? $_.Count) ?? 0) -gt 0 } | Select-Object -ExpandProperty PartitionKey -Unique

Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ function Start-UserTasksOrchestrator {
1111
$TaskId = $null
1212
)
1313

14+
try { [CIPP.TestDataCache]::ClearExpired() } catch { Write-Information "TestDataCache clearexpired skipped: $($_.Exception.Message)" }
15+
1416
$Table = Get-CippTable -tablename 'ScheduledTasks'
1517

1618
if ($TaskId) {

Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ function Start-DurableCleanup {
1818
[int]$MaxDuration = 86400
1919
)
2020

21+
if ($env:CIPPNG -eq 'true') {
22+
return
23+
}
24+
2125
$WarningPreference = 'SilentlyContinue'
2226
$TargetTime = (Get-Date).ToUniversalTime().AddSeconds(-$MaxDuration)
2327
$Context = New-AzDataTableContext -ConnectionString $env:AzureWebJobsStorage

Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest002.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ function Invoke-CippTestGenericTest002 {
5454
$LicList = ($Entry.Value.Licenses | Sort-Object) -join ', '
5555
$null = $Result.Append("| $DisplayName | $LicList |`n")
5656
$DisplayCount++
57-
if ($DisplayCount -ge 100) { break }
57+
if ($DisplayCount -ge 500) { break }
5858
}
5959

60-
if ($UserLicenseMap.Count -gt 100) {
61-
$null = $Result.Append("`n*Showing 100 of $($UserLicenseMap.Count) licensed users.*`n")
60+
if ($UserLicenseMap.Count -gt 500) {
61+
$null = $Result.Append("`n*Showing 500 of $($UserLicenseMap.Count) licensed users.*`n")
6262
}
6363

6464
Add-CippTestResult -TenantFilter $Tenant -TestId 'GenericTest002' -TestType 'Identity' -Status 'Informational' -ResultMarkdown $Result -Risk 'Informational' -Name 'User License Overview' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant Overview'

Shared/CIPPSharp/CIPPTestDataCache.cs

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
using System.Reflection;
77
using System.Text.Json;
88
using System.Threading;
9+
using System.Threading.Tasks;
910

1011
namespace CIPP
1112
{
1213
/// <summary>
13-
/// Process-scoped, thread-safe LRU cache for test data lookups.
14-
/// Shared across all PowerShell runspaces. Bounded by both a byte-size
15-
/// cap (default 50 MB) and a short TTL (default 1 minute) so that test
16-
/// suites running against a single tenant get fast cache hits without
17-
/// accumulating stale Gen2 roots that cause GC thrashing.
14+
/// Host-scoped, thread-safe LRU cache for test data lookups. The DLL is
15+
/// loaded once per Azure Functions host, so every PowerShell worker
16+
/// process on that host shares this exact instance. Bounded by both a
17+
/// byte-size cap (default 100 MB) and a short TTL (default 5 minutes)
18+
/// so that test suites running against a single tenant get fast cache
19+
/// hits without accumulating stale Gen2 roots that cause GC thrashing.
1820
/// </summary>
1921
public static class TestDataCache
2022
{
@@ -28,10 +30,12 @@ public static class TestDataCache
2830
private static readonly Dictionary<string, LinkedListNode<string>> _lruIndex = new(); // key → node
2931
private static readonly object _lruLock = new();
3032
private static long _currentBytes;
31-
private static int _accessCount;
33+
private static long _accessCount;
3234
private static long _hits;
3335
private static long _misses;
3436
private static long _evictions;
37+
private static long _oversized;
38+
private static int _sweepInFlight; // 0 = idle, 1 = a background ClearExpired is running
3539

3640
private sealed class CacheEntry
3741
{
@@ -60,7 +64,11 @@ public static void Configure(long maxBytes = 100L * 1024 * 1024, int ttlSeconds
6064

6165
public static bool TryGet(string key, out object? value)
6266
{
63-
Interlocked.Increment(ref _accessCount);
67+
var count = Interlocked.Increment(ref _accessCount);
68+
// Every ~1000 accesses, kick off a background sweep so TTL-expired
69+
// entries that nobody re-reads still get evicted. CAS-guarded so
70+
// overlapping triggers collapse to a single sweep.
71+
if ((count % 1000) == 0) TryFireBackgroundSweep();
6472

6573
if (_cache.TryGetValue(key, out var entry) && !entry.IsExpired)
6674
{
@@ -95,9 +103,14 @@ public static void Set(string key, object? value)
95103
int itemCount = value is ICollection col ? col.Count : 0;
96104
long sizeBytes = EstimateValueSize(value, itemCount);
97105

98-
// If a single entry exceeds the cap, don't cache it at all
106+
// If a single entry exceeds the cap, don't cache it at all — bump
107+
// _oversized so GetDiagnostics surfaces these silent drops instead
108+
// of leaving callers to chase phantom misses.
99109
if (sizeBytes > _maxBytes)
110+
{
111+
Interlocked.Increment(ref _oversized);
100112
return;
113+
}
101114

102115
// Remove existing entry for this key first
103116
if (_cache.ContainsKey(key))
@@ -170,6 +183,7 @@ public static void Clear()
170183
Interlocked.Exchange(ref _hits, 0);
171184
Interlocked.Exchange(ref _misses, 0);
172185
Interlocked.Exchange(ref _evictions, 0);
186+
Interlocked.Exchange(ref _oversized, 0);
173187
}
174188

175189
/// <summary>
@@ -191,6 +205,40 @@ public static int ClearTenant(string tenantFilter)
191205
return removed;
192206
}
193207

208+
/// <summary>
209+
/// Remove every entry whose TTL has elapsed. Pair to the lazy per-key
210+
/// eviction in TryGet — handles keys that nobody reads again. Safe to
211+
/// call from anywhere; the background sweep triggered by TryGet uses
212+
/// this method.
213+
/// </summary>
214+
public static int ClearExpired()
215+
{
216+
// Snapshot first; RemoveEntry mutates _cache and _lruIndex under _lruLock.
217+
var expiredKeys = new List<string>();
218+
foreach (var kvp in _cache)
219+
{
220+
if (kvp.Value.IsExpired) expiredKeys.Add(kvp.Key);
221+
}
222+
foreach (var key in expiredKeys) RemoveEntry(key);
223+
return expiredKeys.Count;
224+
}
225+
226+
/// <summary>
227+
/// Fire-and-forget a single background ClearExpired sweep. The CAS guard
228+
/// collapses overlapping triggers so we never have more than one sweep
229+
/// running at a time, regardless of read pressure.
230+
/// </summary>
231+
private static void TryFireBackgroundSweep()
232+
{
233+
if (Interlocked.CompareExchange(ref _sweepInFlight, 1, 0) != 0) return;
234+
Task.Run(() =>
235+
{
236+
try { ClearExpired(); }
237+
catch { /* swallow — sweep is best-effort */ }
238+
finally { Interlocked.Exchange(ref _sweepInFlight, 0); }
239+
});
240+
}
241+
194242
public static int Count => _cache.Count;
195243
public static long CurrentBytes => Interlocked.Read(ref _currentBytes);
196244
public static double CurrentMB => Math.Round(Interlocked.Read(ref _currentBytes) / (1024.0 * 1024.0), 2);
@@ -199,6 +247,7 @@ public static int ClearTenant(string tenantFilter)
199247
public static long Hits => Interlocked.Read(ref _hits);
200248
public static long Misses => Interlocked.Read(ref _misses);
201249
public static long Evictions => Interlocked.Read(ref _evictions);
250+
public static long Oversized => Interlocked.Read(ref _oversized);
202251
public static double HitRate => (_hits + _misses) > 0
203252
? Math.Round(_hits * 100.0 / (_hits + _misses), 1) : 0;
204253

@@ -326,12 +375,16 @@ private static long EstimateValueSize(object? value, int itemCount)
326375
/// </summary>
327376
public static CacheDiagnostics GetDiagnostics()
328377
{
329-
var now = DateTime.UtcNow;
330378
var entries = _cache.ToArray(); // snapshot
331379

332380
long totalBytes = 0;
333381
var byType = new Dictionary<string, TypeBucket>();
382+
int active = 0, expired = 0;
383+
DateTime? earliestExpiry = null, latestExpiry = null;
334384

385+
// Single pass — use the SizeBytes stored at insert instead of
386+
// re-running EstimateValueSize (which would JSON-serialize every
387+
// PSObject tree on every diagnostic poll and thrash the LOH).
335388
foreach (var kvp in entries)
336389
{
337390
var parts = kvp.Key.Split('|', 2);
@@ -341,7 +394,7 @@ public static CacheDiagnostics GetDiagnostics()
341394
if (kvp.Value.Value is ICollection col)
342395
itemCount = col.Count;
343396

344-
long entryBytes = EstimateValueSize(kvp.Value.Value, itemCount);
397+
long entryBytes = kvp.Value.SizeBytes;
345398
totalBytes += entryBytes;
346399

347400
if (!byType.TryGetValue(dataType, out var bucket))
@@ -352,12 +405,7 @@ public static CacheDiagnostics GetDiagnostics()
352405
bucket.EntryCount++;
353406
bucket.TotalBytes += entryBytes;
354407
bucket.TotalItems += itemCount;
355-
}
356408

357-
int active = 0, expired = 0;
358-
DateTime? earliestExpiry = null, latestExpiry = null;
359-
foreach (var kvp in entries)
360-
{
361409
if (kvp.Value.IsExpired) { expired++; } else { active++; }
362410
var exp = kvp.Value.ExpiresUtc;
363411
if (earliestExpiry == null || exp < earliestExpiry) earliestExpiry = exp;
@@ -377,9 +425,10 @@ public static CacheDiagnostics GetDiagnostics()
377425
Misses = Interlocked.Read(ref _misses),
378426
HitRate = HitRate,
379427
Evictions = Interlocked.Read(ref _evictions),
428+
Oversized = Interlocked.Read(ref _oversized),
380429
EarliestExpiryUtc = earliestExpiry,
381430
LatestExpiryUtc = latestExpiry,
382-
AccessCount = _accessCount,
431+
AccessCount = Interlocked.Read(ref _accessCount),
383432
TypeBreakdown = byType.Values
384433
.OrderByDescending(b => b.TotalBytes)
385434
.ToList(),
@@ -401,9 +450,10 @@ public class CacheDiagnostics
401450
public long Misses { get; set; }
402451
public double HitRate { get; set; }
403452
public long Evictions { get; set; }
453+
public long Oversized { get; set; }
404454
public DateTime? EarliestExpiryUtc { get; set; }
405455
public DateTime? LatestExpiryUtc { get; set; }
406-
public int AccessCount { get; set; }
456+
public long AccessCount { get; set; }
407457
public List<TypeBucket> TypeBreakdown { get; set; } = new();
408458
}
409459

Shared/CIPPSharp/bin/CIPPSharp.dll

512 Bytes
Binary file not shown.

Tools/Build-DevApiModules.ps1

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ $repoRoot = Split-Path -Parent $toolsRoot
55
$modulesRoot = Join-Path $repoRoot 'Modules'
66
$outputRoot = Join-Path $repoRoot 'Output'
77

8-
Install-Module -Name ModuleBuilder -MaximumVersion 3.1.9 -Scope CurrentUser -Force -AllowClobber
9-
Import-Module -Name ModuleBuilder -Force
8+
Import-Module -Name (Join-Path $toolsRoot 'Metadata\1.5.7\Metadata.psd1') -Force
9+
Import-Module -Name (Join-Path $toolsRoot 'Configuration\1.6.0\Configuration.psd1') -Force
10+
Import-Module -Name (Join-Path $toolsRoot 'ModuleBuilder\3.1.8\ModuleBuilder.psd1') -Force
1011

1112
Write-Host "Repo root: $repoRoot"
1213
Set-Location -Path $repoRoot

0 commit comments

Comments
 (0)