Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/scripts/gh-actions.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,11 @@ END
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "sqladmin", $password

$azureUrl = "https://dbatools.blob.core.windows.net/dbatools"
$dbName = "dbatoolsci_logship_azure"
# GITHUB_RUN_ID uniquely identifies this CI job; appending it to the database name keeps
# concurrent log-shipping tests from picking the same Azure blob name on the shared
# container. GITHUB_RUN_ATTEMPT is appended so a rerun still gets its own database.
# See https://github.com/dataplat/dbatools/issues/10667.
$dbName = "dbatoolsci_logship_azure_$($env:GITHUB_RUN_ID)_$($env:GITHUB_RUN_ATTEMPT)"

# Create SAS token credential on both instances
$primaryServer = Connect-DbaInstance -SqlInstance localhost -SqlCredential $cred
Expand Down Expand Up @@ -507,7 +511,10 @@ END
It -Skip:(-not $env:azurepasswd) "adds a second live secondary without replacing the Azure primary configuration" {
$PSDefaultParameterValues.Clear()
$azureUrl = "https://dbatools.blob.core.windows.net/dbatools"
$dbName = "dbatoolsci_logship_addsecondary"
# GITHUB_RUN_ID uniquely identifies this CI job; the addsecondary test
# uploads to the same shared Azure container, so its database name also
# needs the per-run suffix. See https://github.com/dataplat/dbatools/issues/10667.
$dbName = "dbatoolsci_logship_addsecondary_$($env:GITHUB_RUN_ID)_$($env:GITHUB_RUN_ATTEMPT)"
$secondDbName = "${dbName}_second"
$missingPrimaryDbName = "${dbName}_missing"
$sasToken = $env:azurepasswd.TrimStart("?")
Expand Down
2 changes: 1 addition & 1 deletion public/Invoke-DbaDbLogShipping.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1660,7 +1660,7 @@ WHERE pd.primary_database = N'$escapedPrimaryDatabase'
Write-Message -Message "Backing up database $db to $DatabaseSharedPath" -Level Verbose

try {
$Timestamp = Get-Date -format "yyyyMMddHHmmss"
$Timestamp = Get-Date -Format "yyyyMMddHHmmssfff"

if ($UseAzure) {
# Backup to Azure blob storage - use container base URL only
Expand Down
82 changes: 82 additions & 0 deletions tests/Invoke-DbaDbLogShipping.Azure.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#Requires -Module @{ ModuleName = "Pester"; ModuleVersion = "5.0" }
<#
Regression coverage for the Azure log shipping blob name collision reported
in https://github.com/dataplat/dbatools/issues/10667.

Two concurrent CI runs that reach Invoke-DbaDbLogShipping within the same
wall-clock second used to write the same FullBackup_PreLogShipping blob to
the shared Azure container, because:

1. The production timestamp was built with Get-Date -Format "yyyyMMddHHmmss"
(second resolution).
2. The CI test pinned the database names ("dbatoolsci_logship_azure" and
"dbatoolsci_logship_addsecondary"), so the per-second collision was not
bounded to a single runner.

The Azure test path needs a real SQL Server plus a real Azure container, so
the actual collision cannot be reproduced in a unit environment. The
regression is therefore asserted on the shape of the inputs that drive the
blob name: sub-second timestamp precision in the production cmdlet, and
per-run unique database names in the CI script.
#>
param(
$ModuleName = "dbatools",
$CommandName = "Invoke-DbaDbLogShipping",
$PSDefaultParameterValues = $TestConfig.Defaults
)

Describe "$CommandName - Azure blob name collision (#10667)" -Tag UnitTests {
BeforeAll {
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$ProductionFile = Join-Path $RepoRoot "public/Invoke-DbaDbLogShipping.ps1"
$CiTestScriptFile = Join-Path $RepoRoot ".github/scripts/gh-actions.ps1"
}

Context "Production timestamp must include sub-second precision" {
It "uses a millisecond format specifier for the Azure pre-log-shipping timestamp" {
$ProductionFile | Should -Exist

$productionContent = Get-Content -Path $ProductionFile -Raw

# The offending line built the timestamp with second resolution only.
# It must contain the millisecond 'fff' format specifier so two
# callers that enter the cmdlet inside the same second no longer
# produce the same Azure blob name.
$timestampMatches = [regex]::Matches(
$productionContent,
'Get-Date\s+(?:-Format|-format)\s+"(?<fmt>[^"]+)"'
)
$azureTimestampLine = $timestampMatches | Where-Object {
$PSItem.Groups['fmt'].Value -like '*yyyyMMddHHmmss*' -and
$PSItem.Groups['fmt'].Value -like '*fff*'
} | Select-Object -First 1

$timestampReason = "Invoke-DbaDbLogShipping must build the Azure pre-log-shipping timestamp with sub-second precision to avoid the collision in #10667"
$azureTimestampLine | Should -Not -BeNullOrEmpty -Because $timestampReason
}
}

Context "CI test database names must be unique per run" {
It "scopes every log shipping database name with GITHUB_RUN_ID" {
$CiTestScriptFile | Should -Exist

$ciScriptContent = Get-Content -Path $CiTestScriptFile -Raw

# Every log shipping database name in the CI script must include a
# per-run token (GITHUB_RUN_ID, plus GITHUB_RUN_ATTEMPT for reruns)
# so two simultaneous CI jobs never collide on the shared Azure
# container. This covers both "dbatoolsci_logship_azure" and
# "dbatoolsci_logship_addsecondary".
$dbNameMatches = [regex]::Matches(
$ciScriptContent,
'\$dbName\s*=\s*"(?<value>dbatoolsci_logship_[^"]+)"'
)
$dbNameMatches | Should -Not -BeNullOrEmpty -Because "the Azure log shipping integration tests must declare per-run database names"

foreach ($dbNameMatch in $dbNameMatches) {
$dbNameReason = "the database name used by the Azure log shipping tests must be unique per CI run (found: $($dbNameMatch.Groups['value'].Value))"
$dbNameMatch.Groups['value'].Value | Should -Match 'GITHUB_RUN_ID' -Because $dbNameReason
}
}
}
}
Loading