Skip to content

Commit 73a4311

Browse files
committed
Improve scheduled task handling and rerun protection
Adds duplicate RowKey checks to prevent race conditions when creating scheduled tasks. Enhances rerun protection logic in Push-ExecScheduledCommand to avoid duplicate executions within recurrence intervals. Refines orchestrator task state transitions and filtering for stuck tasks. Improves logging and filtering for scheduled item listing, and updates Test-CIPPRerun to support custom intervals and base times for scheduled tasks.
1 parent 9846930 commit 73a4311

File tree

7 files changed

+94
-21
lines changed

7 files changed

+94
-21
lines changed

Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ function Add-CIPPScheduledTask {
4646
return "Could not run task: $ErrorMessage"
4747
}
4848
} else {
49+
# Generate RowKey early to use in duplicate check
50+
if (!$Task.RowKey) {
51+
$RowKey = (New-Guid).Guid
52+
} else {
53+
$RowKey = $Task.RowKey
54+
}
55+
56+
# Check for duplicate RowKey (prevents race conditions)
57+
$Filter = "PartitionKey eq 'ScheduledTask' and RowKey eq '$RowKey'"
58+
$ExistingTaskByKey = (Get-CIPPAzDataTableEntity @Table -Filter $Filter)
59+
if ($ExistingTaskByKey) {
60+
return "Task with ID $RowKey already exists"
61+
}
62+
4963
if ($DisallowDuplicateName) {
5064
$Filter = "PartitionKey eq 'ScheduledTask' and Name eq '$($Task.Name)'"
5165
$ExistingTask = (Get-CIPPAzDataTableEntity @Table -Filter $Filter)
@@ -110,11 +124,8 @@ function Add-CIPPScheduledTask {
110124
}
111125
$AdditionalProperties = ([PSCustomObject]$AdditionalProperties | ConvertTo-Json -Compress)
112126
if ($Parameters -eq 'null') { $Parameters = '' }
113-
if (!$Task.RowKey) {
114-
$RowKey = (New-Guid).Guid
115-
} else {
116-
$RowKey = $Task.RowKey
117-
}
127+
128+
# RowKey already generated during duplicate check above
118129

119130
$Recurrence = if ([string]::IsNullOrEmpty($task.Recurrence.value)) {
120131
$task.Recurrence

Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,48 @@ function Push-ExecScheduledCommand {
3535
Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue
3636
return
3737
}
38+
# Task should be 'Pending' (queued by orchestrator) or 'Running' (retry/recovery)
39+
# We accept both to handle edge cases
40+
41+
# Check for rerun protection - prevent duplicate executions within the recurrence interval
42+
if ($task.Recurrence -and $task.Recurrence -ne '0') {
43+
# Calculate interval in seconds from recurrence string
44+
$IntervalSeconds = switch -Regex ($task.Recurrence) {
45+
'^(\d+)$' { [int64]$matches[1] * 86400 } # Plain number = days
46+
'(\d+)m$' { [int64]$matches[1] * 60 }
47+
'(\d+)h$' { [int64]$matches[1] * 3600 }
48+
'(\d+)d$' { [int64]$matches[1] * 86400 }
49+
default { 0 }
50+
}
51+
52+
if ($IntervalSeconds -gt 0) {
53+
# Round down to nearest 15-minute interval (900 seconds) since that's when orchestrator runs
54+
# This prevents rerun blocking issues due to slight timing variations
55+
$FifteenMinutes = 900
56+
$AdjustedInterval = [Math]::Floor($IntervalSeconds / $FifteenMinutes) * $FifteenMinutes
57+
58+
# Ensure we have at least one 15-minute interval
59+
if ($AdjustedInterval -lt $FifteenMinutes) {
60+
$AdjustedInterval = $FifteenMinutes
61+
}
62+
# Use task RowKey as API identifier for rerun cache
63+
$RerunParams = @{
64+
TenantFilter = $Tenant
65+
Type = 'ScheduledTask'
66+
API = $task.RowKey
67+
Interval = $AdjustedInterval
68+
BaseTime = [int64]$task.ScheduledTime
69+
Headers = $Headers
70+
}
71+
72+
$IsRerun = Test-CIPPRerun @RerunParams
73+
if ($IsRerun) {
74+
Write-Information "Scheduled task $($task.Name) for tenant $Tenant was recently executed. Skipping to prevent duplicate execution."
75+
Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue
76+
return
77+
}
78+
}
79+
}
3880

3981
if ($task.Trigger) {
4082
# Extract trigger data from the task and process

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecAppInsightsQuery.ps1

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,12 @@ function Invoke-ExecAppInsightsQuery {
2424
try {
2525
$LogData = Get-ApplicationInsightsQuery -Query $Query
2626

27-
$Body = @{
27+
$Body = ConvertTo-Json -Depth 10 -Compress -InputObject @{
2828
Results = @($LogData)
2929
Metadata = @{
3030
Query = $Query
3131
}
32-
} | ConvertTo-Json -Depth 10 -Compress
33-
32+
}
3433
return [HttpResponseContext]@{
3534
StatusCode = [HttpStatusCode]::OK
3635
Body = $Body

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function Invoke-AddScheduledItem {
3232
$ScheduledTask = @{
3333
Task = $Request.Body
3434
Headers = $Request.Headers
35-
hidden = $hidden
35+
Hidden = $hidden
3636
DisallowDuplicateName = $Request.Query.DisallowDuplicateName
3737
DesiredStartTime = $Request.Body.DesiredStartTime
3838
}

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ function Invoke-ListScheduledItems {
2222
$SearchTitle = $Request.query.SearchTitle ?? $Request.body.SearchTitle
2323

2424
if ($ShowHidden -eq $true) {
25-
$ScheduledItemFilter.Add('Hidden eq true')
25+
$ScheduledItemFilter.Add("(Hidden eq true or Hidden eq 'True')")
2626
} else {
27-
$ScheduledItemFilter.Add('Hidden eq false')
27+
$ScheduledItemFilter.Add("(Hidden eq false or Hidden eq 'False')")
2828
}
2929

3030
if ($Name) {
@@ -43,6 +43,7 @@ function Invoke-ListScheduledItems {
4343
$HiddenTasks = $true
4444
}
4545
$Tasks = Get-CIPPAzDataTableEntity @Table -Filter $Filter
46+
Write-Information "Retrieved $($Tasks.Count) scheduled tasks from storage."
4647
if ($Type) {
4748
$Tasks = $Tasks | Where-Object { $_.command -eq $Type }
4849
}
@@ -58,8 +59,12 @@ function Invoke-ListScheduledItems {
5859
$AllowedTenantDomains = $TenantList | Where-Object -Property customerId -In $AllowedTenants | Select-Object -ExpandProperty defaultDomainName
5960
$Tasks = $Tasks | Where-Object -Property Tenant -In $AllowedTenantDomains
6061
}
61-
$ScheduledTasks = foreach ($Task in $tasks) {
62+
63+
Write-Information "Found $($Tasks.Count) scheduled tasks after filtering and access check."
64+
65+
$ScheduledTasks = foreach ($Task in $Tasks) {
6266
if (!$Task.Tenant -or !$Task.Command) {
67+
Write-Information "Skipping invalid scheduled task entry: $($Task.RowKey)"
6368
continue
6469
}
6570

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ function Start-UserTasksOrchestrator {
1010
param()
1111

1212
$Table = Get-CippTable -tablename 'ScheduledTasks'
13-
$1HourAgo = (Get-Date).AddHours(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
14-
$Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Running' and Timestamp lt datetime'$1HourAgo'))"
13+
$30MinutesAgo = (Get-Date).AddMinutes(-30).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
14+
$4HoursAgo = (Get-Date).AddHours(-4).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
15+
# Pending = orchestrator queued, Running = actively executing
16+
# Pick up: Planned, Failed-Planned, stuck Pending (>30min), or stuck Running (>4hr for large AllTenants tasks)
17+
$Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Pending' and Timestamp lt datetime'$30MinutesAgo') or (TaskState eq 'Running' and Timestamp lt datetime'$4HoursAgo'))"
1518
$tasks = Get-CIPPAzDataTableEntity @Table -Filter $Filter
1619

1720
$RateLimitTable = Get-CIPPTable -tablename 'SchedulerRateLimits'
@@ -49,11 +52,14 @@ function Start-UserTasksOrchestrator {
4952
$currentUnixTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds
5053
if ($currentUnixTime -ge $task.ScheduledTime) {
5154
try {
55+
# Update task state to 'Pending' immediately to prevent concurrent orchestrator runs from picking it up
56+
# 'Pending' = orchestrator has picked it up and is queuing commands
57+
# 'Running' = actual execution is happening (set by Push-ExecScheduledCommand)
5258
$null = Update-AzDataTableEntity -Force @Table -Entity @{
5359
PartitionKey = $task.PartitionKey
5460
RowKey = $task.RowKey
5561
ExecutedTime = "$currentUnixTime"
56-
TaskState = 'Planned'
62+
TaskState = 'Pending'
5763
}
5864
$task.Parameters = $task.Parameters | ConvertFrom-Json -AsHashtable
5965
$task.AdditionalProperties = $task.AdditionalProperties | ConvertFrom-Json

Modules/CIPPCore/Public/Test-CIPPRerun.ps1

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,25 @@ function Test-CIPPRerun {
77
$Settings,
88
$Headers,
99
[switch]$Clear,
10-
[switch]$ClearAll
10+
[switch]$ClearAll,
11+
[int64]$Interval = 0, # Custom interval in seconds (for scheduled tasks)
12+
[int64]$BaseTime = 0 # Base time to calculate from (defaults to current time)
1113
)
1214
$RerunTable = Get-CIPPTable -tablename 'RerunCache'
13-
$EstimatedDifference = switch ($Type) {
14-
'Standard' { 9800 } # 2 hours 45 minutes ish.
15-
'BPA' { 85000 } # 24 hours ish.
16-
default { throw "Unknown type: $Type" }
15+
16+
# Use custom interval if provided, otherwise use type-based defaults
17+
if ($Interval -gt 0) {
18+
$EstimatedDifference = $Interval
19+
} else {
20+
$EstimatedDifference = switch ($Type) {
21+
'Standard' { 9800 } # 2 hours 45 minutes ish.
22+
'BPA' { 85000 } # 24 hours ish.
23+
default { throw "Unknown type: $Type" }
24+
}
1725
}
18-
$CurrentUnixTime = [int][double]::Parse((Get-Date -UFormat %s))
26+
27+
# Use BaseTime if provided, otherwise use current time
28+
$CurrentUnixTime = if ($BaseTime -gt 0) { $BaseTime } else { [int][double]::Parse((Get-Date -UFormat %s)) }
1929
$EstimatedNextRun = $CurrentUnixTime + $EstimatedDifference
2030

2131
try {

0 commit comments

Comments
 (0)