Skip to content

Commit 5b062cd

Browse files
Export-DbaCsv - Close the reader in the finally when writing the file fails (#10790)
1 parent 01c262f commit 5b062cd

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

‎public/Export-DbaCsv.ps1‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ function Export-DbaCsv {
300300

301301
# The finally makes sure the connection is also closed when the export fails and the catch
302302
# continues with the next instance, and when -WhatIf skips the export altogether.
303+
$cmd = $null
304+
$reader = $null
303305
try {
304306
$sqlToExecute = $null
305307

@@ -355,6 +357,19 @@ function Export-DbaCsv {
355357
}
356358
}
357359
} finally {
360+
# The reader is only closed inside the try after a successful write. When the writer fails, the reader
361+
# stays open on the connection with the rest of the rows unread. A connection the caller passed in is
362+
# not disconnected below, so their next query on it fails because of the open reader.
363+
if ($reader -and -not $reader.IsClosed) {
364+
try {
365+
# Cancel the command first: closing a reader with unread rows would otherwise drain the whole
366+
# remaining result set from the server before it returns.
367+
$cmd.Cancel()
368+
$reader.Close()
369+
} catch {
370+
Write-Message -Level Debug -Message "Failed to close the reader: $PSItem"
371+
}
372+
}
358373
if ($isNewConnection) {
359374
# -WhatIf:$false because this is ownership bookkeeping, not the user's operation:
360375
# Disconnect-DbaInstance supports ShouldProcess, so a propagated $WhatIfPreference

‎tests/Export-DbaCsv.Tests.ps1‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,63 @@ where program_name like 'dbatools%'
386386
}
387387
}
388388

389+
Context "When writing the file fails on a connection of the caller" {
390+
BeforeAll {
391+
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
392+
# The command does not disconnect a server object it was given, so only its own cleanup can close the
393+
# reader on that connection. Connect-DbaInstance hands back that object itself when it already is in the
394+
# database the command asks for, master here; for any other database the command works on a copy of its
395+
# own, which it disconnects. The second connection checks the server from outside.
396+
$callerServer = Connect-DbaInstance -SqlInstance $TestConfig.InstanceSingle -NonPooledConnection
397+
$checkServer = Connect-DbaInstance -SqlInstance $TestConfig.InstanceSingle -NonPooledConnection
398+
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
399+
400+
# The file is held open without sharing, so the CSV writer cannot open it. The writer is created after
401+
# the query has started, and the cross join makes sure the server has far more rows to send than the
402+
# client has read by then. The marker in the SELECT finds the request afterwards, the session of the
403+
# check is excluded because the text of the check contains the marker too.
404+
$lockedPath = "$testExportPath\locked-$(Get-Random).csv"
405+
$lockedFile = [System.IO.File]::Open($lockedPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
406+
$readerQuery = "SELECT 'dbatoolsci_csvreader_marker' AS id FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b"
407+
$readerRequestQuery = "SELECT r.session_id FROM sys.dm_exec_requests AS r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t WHERE t.text LIKE '%dbatoolsci_csvreader_marker%' AND r.session_id <> @@SPID"
408+
try {
409+
$splatLockedExport = @{
410+
SqlInstance = $callerServer
411+
Database = "master"
412+
Query = $readerQuery
413+
Path = $lockedPath
414+
WarningAction = "SilentlyContinue"
415+
}
416+
$lockedResult = Export-DbaCsv @splatLockedExport
417+
$lockedWarning = $WarnVar
418+
} finally {
419+
$lockedFile.Dispose()
420+
}
421+
}
422+
423+
AfterAll {
424+
# A request left behind by the old code would keep running until its session is gone.
425+
foreach ($leakedRequest in $checkServer.Query($readerRequestQuery)) {
426+
$null = $checkServer.Query("KILL $($leakedRequest.session_id)")
427+
}
428+
$callerServer.ConnectionContext.Disconnect()
429+
$checkServer.ConnectionContext.Disconnect()
430+
}
431+
432+
It "Warns and returns nothing" {
433+
$lockedResult | Should -BeNullOrEmpty
434+
($lockedWarning -join " ") | Should -BeLike "*Failed to export data from*"
435+
}
436+
437+
It "Stops the query on the server" {
438+
@($checkServer.Query($readerRequestQuery)).Count | Should -Be 0
439+
}
440+
441+
It "Leaves the connection of the caller usable" {
442+
$callerServer.Query("SELECT 1 AS one").one | Should -Be 1
443+
}
444+
}
445+
389446
Context "Compression options (issue #8646)" {
390447
It "exports with each compression type" {
391448
$compressionTypes = @("None", "GZip", "Deflate")

0 commit comments

Comments
 (0)