Users: 15,000+ | Page Views: 50,000+ | PPS: 2.5
TechDevOps.com
Explore Tools
{Experts in Microsoft SQL Server Operations, Performance, HA/DR, Data Migrations & Cloud Computing}



Log Shipping Real-Time Processing Engine
by BF (Senior MSSQL Operations - Enterprise, Cloud, Strategy)
2018-11-02








--Execute on Log Shipping Secondary Server


USE Master;
GO

SET NOCOUNT ON

DECLARE @backupPath NVARCHAR(500)
DECLARE @cmd NVARCHAR(500)
DECLARE @fileList1 TABLE (backupFile NVARCHAR(255))
DECLARE @fileList2 TABLE (backupFile NVARCHAR(255))
DECLARE @backupFile NVARCHAR(500)
DECLARE @IsDB1 bit
DECLARE @IsDB2 bit
DECLARE @IsDB3 bit
DECLARE @IsDB4 bit
DECLARE @IsDB5 bit
DECLARE @IsDB6 bit
DECLARE @IsDB7 bit
DECLARE @jobname varchar(150)
SET @backupPath = 'F:\LogShipping\Backups'

SELECT
reh.Destination_Database_Name, reh.Restore_Type, reh.User_Name, reh.Restore_Date,
bas.First_LSN, bas.Last_LSN, bas.Backup_Start_Date, bas.Backup_Finish_Date,
bmf.Physical_Device_Name,
LEFT(bmf.Physical_Device_Name,LEN(bmf.Physical_Device_Name) - charindex('\',reverse(bmf.Physical_Device_Name),1) + 1) as 'path',
RIGHT(bmf.Physical_Device_Name, CHARINDEX('\', REVERSE(bmf.Physical_Device_Name)) -1) as 'file_name',
bas.Server_Name, bas.Recovery_Model, bas.Compressed_Backup_Size,
bms.Is_Compressed, bas.Compatibility_Level
INTO #TMP1
FROM msdb.dbo.RESTOREHISTORY reh WITH (nolock)
Inner Join msdb.dbo.backupset bas on reh.backup_set_id = bas.backup_set_id
Inner Join msdb.dbo.backupmediaset bms on bas.backup_set_id = bms.media_set_id
Inner Join msdb.dbo.backupmediafamily bmf on bas.media_set_id = bmf.media_set_id

SET @cmd = 'DIR /b "' + @backupPath + '"'

INSERT INTO @fileList1(backupFile)
EXEC master.sys.xp_cmdshell @cmd

Delete from @filelist1 where backupfile IS NULL

PRINT CHAR(10)

PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
PRINT '>>> Starting - Log Shipping Processing check: Backup File --> Copy File --> Restore File'
PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'

PRINT CHAR(10)

PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
PRINT '>>> List File System Backup Files Outstanding:'
PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
PRINT CHAR(10)

If NOT EXISTS (Select BackupFile from @fileList1 where backupFile not in (Select [file_name] from #TMP1))
BEGIN

PRINT ' - No Backup Files to process'

PRINT CHAR(10)

PRINT '>>> Finished'

DROP table #TMP1

RETURN

END

--Select BackupFile from @fileList1 where backupFile not in (Select [file_name] from #TMP1) order by backupFile asc

DECLARE @name1 nvarchar(128)

DECLARE cur1 CURSOR FOR
Select BackupFile from @fileList1 where backupFile not in (Select [file_name] from #TMP1) order by backupFile asc

OPEN cur1

FETCH NEXT FROM cur1 INTO @name1;

WHILE @@FETCH_STATUS = 0
BEGIN

IF CHARINDEX('DB1',@name1) > 0 Begin Set @IsDB1 = 1 PRINT 'DB1:' + CHAR(10) + @name1 End;
IF CHARINDEX('DB2',@name1) > 0 Begin Set @IsDB2 = 1 PRINT 'DB2:' + CHAR(10) + @name1 End;
IF CHARINDEX('DB3',@name1) > 0 Begin Set @IsDB3 = 1 PRINT 'DB3:' + CHAR(10) + @name1 End;
IF CHARINDEX('DB4',@name1) > 0 Begin Set @IsDB4 = 1 PRINT 'DB4:' + CHAR(10) + @name1 End;
IF CHARINDEX('DB5',@name1) > 0 Begin Set @IsDB5 = 1 PRINT 'DB5:' + CHAR(10) + @name1 End;
IF CHARINDEX('DB6',@name1) > 0 Begin Set @IsDB6 = 1 PRINT 'DB6:' + CHAR(10) + @name1 End;
IF CHARINDEX('DB7',@name1) > 0 Begin Set @IsDB7 = 1 PRINT 'DB7:' + CHAR(10) + @name1 End;

FETCH NEXT FROM cur1 INTO @name1;
END

CLOSE cur1;
DEALLOCATE cur1;

PRINT CHAR(10)

PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
PRINT '>>> Starting Log Shipping Copy Job(s)...'
PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
PRINT CHAR(10)

If @IsDB1 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name--,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Copy%' AND sj.name like '%DB1%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Copy%' AND name like '%DB1%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End

If @IsDB2 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name--,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Copy%' AND sj.name like '%DB2%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Copy%' AND name like '%DB2%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End

If @IsDB3 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name--,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Copy%' AND sj.name like '%DB3%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Copy%' AND name like '%DB3%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End

If @IsDB4 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name--,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Copy%' AND sj.name like '%DB4%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Copy%' AND name like '%DB4%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End


If @IsDB5 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name--,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Copy%' AND sj.name like '%DB5%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Copy%' AND name like '%DB5%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End

If @IsDB6 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name--,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Copy%' AND sj.name like '%DB6%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Copy%' AND name like '%DB6%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End

If @IsDB7 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name--,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Copy%' AND sj.name like '%DB7%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Copy%' AND name like '%DB7%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End


PRINT CHAR(10)

PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
PRINT '>>> Delay for 5s for Copy File processing...'
PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'

WAITFOR DELAY '00:00:05';

PRINT CHAR(10)

PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
PRINT '>>> Starting Log Shipping Restore Job(s)...'
PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'

PRINT CHAR(10)

If @IsDB1 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Restore%' AND sj.name like '%DB1%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Restore%' AND name like '%DB1%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End


If @IsDB2 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Restore%' AND sj.name like '%DB2%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Restore%' AND name like '%DB2%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End


If @IsDB3 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Restore%' AND sj.name like '%DB3%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Restore%' AND name like '%DB3%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End

If @IsDB4 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Restore%' AND sj.name like '%DB4%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Restore%' AND name like '%DB4%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End

If @IsDB5 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Restore%' AND sj.name like '%DB5%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Restore%' AND name like '%DB5%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End

If @IsDB6 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Restore%' AND sj.name like '%DB6%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Restore%' AND name like '%DB6%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End

If @IsDB7 = 1
Begin
IF NOT EXISTS
(
SELECT sj.name,DATEDIFF(SECOND,aj.start_execution_date,GetDate()) AS Seconds
FROM msdb..sysjobactivity aj
JOIN msdb..sysjobs sj on sj.job_id = aj.job_id
WHERE aj.stop_execution_date IS NULL -- job hasn't stopped running
AND aj.start_execution_date IS NOT NULL -- job is currently running
AND aj.run_Requested_date IS NOT NULL
AND sj.name like '%Restore%' AND sj.name like '%DB7%'
and not exists(
SELECT 1 from msdb..sysjobactivity new
WHERE new.job_id = aj.job_id
AND new.start_execution_date > aj.start_execution_date )
)
Begin
Set @jobname = (Select name from msdb.dbo.sysjobs where category_id = 6 and name like '%Restore%' AND name like '%DB7%')
EXEC msdb.dbo.sp_start_job @jobname;
End
Else Begin Print '>>> Job is already running - ' + @jobname End
End

PRINT CHAR(10)

PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
PRINT '>>> Delay for 5s for Restore File processing...'
PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'

WAITFOR DELAY '00:00:05';

PRINT CHAR(10)

SELECT
reh.Destination_Database_Name, reh.Restore_Type, reh.User_Name, reh.Restore_Date,
bas.First_LSN, bas.Last_LSN, bas.Backup_Start_Date, bas.Backup_Finish_Date,
bmf.Physical_Device_Name,
LEFT(bmf.Physical_Device_Name,LEN(bmf.Physical_Device_Name) - charindex('\',reverse(bmf.Physical_Device_Name),1) + 1) as 'path',
RIGHT(bmf.Physical_Device_Name, CHARINDEX('\', REVERSE(bmf.Physical_Device_Name)) -1) as 'file_name',
bas.Server_Name, bas.Recovery_Model, bas.Compressed_Backup_Size,
bms.Is_Compressed, bas.Compatibility_Level
INTO #TMP2
FROM msdb.dbo.RESTOREHISTORY reh WITH (nolock)
Inner Join msdb.dbo.backupset bas on reh.backup_set_id = bas.backup_set_id
Inner Join msdb.dbo.backupmediaset bms on bas.backup_set_id = bms.media_set_id
Inner Join msdb.dbo.backupmediafamily bmf on bas.media_set_id = bmf.media_set_id

SET @cmd = 'DIR /b "' + @backupPath + '"'

INSERT INTO @fileList2(backupFile)
EXEC master.sys.xp_cmdshell @cmd

Delete from @filelist2 where backupfile IS NULL

PRINT CHAR(10)

PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
PRINT '>>> Start - List File System Backup Files Outstanding:'
PRINT '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
PRINT CHAR(10)

If NOT EXISTS (Select BackupFile from @fileList2 where backupFile not in (Select [file_name] from #TMP2))
BEGIN

PRINT ' - No Backup Files to process'

PRINT CHAR(10)

PRINT '>>> Finished'

DROP Table #TMP1
DROP Table #TMP2

RETURN

END

DECLARE @name2 nvarchar(128)

DECLARE cur2 CURSOR FOR
Select BackupFile from @fileList2 where backupFile not in (Select [file_name] from #TMP2) order by backupFile asc

OPEN cur2

FETCH NEXT FROM cur2 INTO @name2;
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT @name2
FETCH NEXT FROM cur2 INTO @name2;
END

CLOSE cur2;
DEALLOCATE cur2;

Drop Table #TMP1
Drop Table #TMP2

PRINT CHAR(10)

PRINT '>>> Finished'