Replicating Jobs Between SQL Instances – Schedules
This is part 4 on the series of how to replicate SQL Jobs between instances.
Previously I’ve covered why replicate, setting up and replicating categories. The next item to replicate is Schedules.
SQL Agent Schedules are used to tell SQL Agent Jobs when they should run. When you update a schedule, it’s not enough just to change the data in the msdb table. You need to notify SQL Agent that the data has changed. The better option is to always use the built in stored procs to add or update schedules. The built in stored procs will update the data and notify SQL Agent that a change has occurred.
The biggest problem when replicating schedules is working how to determine if the schedule already exists on the target server. The update stored procedure requires that you send through an ID or a name, but the name does not have to be unique. Why Microsoft designed it this way, I do not know, but it causes problems for the replication. You cannot update just on name alone because it’s not unique, but there is also no way to provide a unique ID, there is only the Identity Column that is automatically generated.
The approach we took was to create all schedules on the Primary server only, there are no schedules on the secondaries that are not created from the primary, and so we use schedule_id to compare schedules.
This has 2 risks:
- If a schedule is created on the target computer with the same id as the primary then it will be overwritten;
- If the schedule ID on the primary is less than the ID on the target then duplicate schedules will just continue to be created.
If you only ever create schedules on the primary server then neither of these will be a problem.
On to the code.
1. Setup
This is the same as the setup for categories. The script checks that it isn’t running on the primary server, creates a variable with the name of the primary server and creates a temp table.
IF 0 = (SELECT master.dbo.fCheckIfPrimary('PrimaryReplicaName')) BEGIN DECLARE @PartnerServer sysname = 'PrimaryReplicaName' DECLARE @Machine sysname DECLARE @SQL NVARCHAR(MAX) If CharIndex('', @PartnerServer) > 0 Begin Set @Machine = LEFT(@PartnerServer, CharIndex('', @PartnerServer) - 1); End Else Begin Set @Machine = @PartnerServer; End --update schedules CREATE TABLE #Schedules ( Schedule_id int, ScheduleName sysname, Enabled tinyint, Freq_type int, Freq_interval int, freq_subday_type int, freq_subday_interval int, freq_relative_interval int, freq_recurrence_factor int, active_start_date int, active_end_date int, active_start_trim int, active_end_Time int, owner_login_name nvarchar(128), Action varchar(6) ) --Find Schedules to Copy --Copy END
2. Find
Similar to the categories, just a bit more complex. Check if the schedule on the primary exists with the exact same setup on the replica.
IF 0 = (SELECT master.dbo.fCheckIfPrimary('PrimaryReplicaName')) BEGIN --SETUP SET @SQL = CAST(' SELECT * FROM ( SELECT msdbPrimary.schedule_id, msdbPrimary.name, msdbPrimary.Enabled, msdbPrimary.Freq_type, msdbPrimary.Freq_interval, msdbPrimary.freq_subday_type, msdbPrimary.freq_subday_interval , msdbPrimary.freq_relative_interval , msdbPrimary.freq_recurrence_factor , msdbPrimary.active_start_date , msdbPrimary.active_end_date , msdbPrimary.active_start_time , msdbPrimary.active_end_Time , SUSER_NAME(msdbPrimary.owner_sid) as ownerloginname ,CASE WHEN msdbLocal.schedule_id IS NULL THEN ''INSERT'' WHEN msdbLocal.schedule_id IS NOT NULL AND ( msdbLocal.name <> msdbPrimary.name OR msdbLocal.Enabled <> msdbPrimary.Enabled OR msdbLocal.Freq_type <> msdbPrimary.Freq_type OR msdbLocal.Freq_interval <> msdbPrimary.Freq_interval OR msdbLocal.freq_subday_type <> msdbPrimary.freq_subday_type OR msdbLocal.freq_subday_interval <> msdbPrimary.freq_subday_interval OR msdbLocal.freq_relative_interval <> msdbPrimary.freq_relative_interval OR msdbLocal.freq_recurrence_factor <> msdbPrimary.freq_recurrence_factor OR msdbLocal.active_start_date <> msdbPrimary.active_start_date OR msdbLocal.active_end_date <> msdbPrimary.active_end_date OR msdbLocal.active_start_time <> msdbPrimary.active_start_time OR msdbLocal.active_end_Time <> msdbPrimary.active_end_Time OR SUSER_NAME(msdbPrimary.owner_sid) <> SUSER_NAME(msdbPrimary.owner_sid) ) THEN ''UPDATE'' ELSE NULL END as Action FROM ' + QUOTENAME(@Machine) + '.msdb.dbo.sysSchedules as msdbPrimary LEFT JOIN msdb.dbo.sysSchedules msdbLocal ON msdbPrimary.schedule_id = msdbLocal.schedule_id ) InsertUpdate WHERE Action IS NOT NULL ' AS varchar(MAX)) INSERT INTO #Schedules EXEC sp_ExecuteSQL @SQL --Copy END
3. Copy
As discussed in the categories post, the easiest way to copy these is with a cursor so you can call the insert or update stored proc on every record that needs inserting or updating.
IF 0 = (SELECT master.dbo.fCheckIfPrimary('PrimaryReplicaName')) BEGIN --Setup --Find Categories to Copy DECLARE @schedule_id INT, @schedule_name SYSNAME, @enabled TINYINT = 1, -- Name does not have to be unique @freq_type INT = 0, @freq_interval INT = 0, @freq_subday_type INT = 0, @freq_subday_interval INT = 0, @freq_relative_interval INT = 0, @freq_recurrence_factor INT = 0, @active_start_date INT = NULL, -- sp_verify_schedule assigns a default @active_end_date INT = 99991231, -- December 31st 9999 @active_start_time INT = 000000, -- 12:00:00 am @active_end_time INT = 235959, -- 11:59:59 pm @owner_login_name sysname = NULL, @Action VARCHAR(6) DECLARE db_cursor CURSOR FOR SELECT Schedule_ID, ScheduleName, Enabled, Freq_type, Freq_interval, freq_subday_type, freq_subday_interval, freq_relative_interval, freq_recurrence_factor, active_start_date, active_end_date, active_start_trim, active_end_Time, owner_login_name, Action FROM #Schedules OPEN db_cursor FETCH NEXT FROM db_cursor INTO @Schedule_id ,@schedule_name ,@enabled ,@freq_type ,@freq_interval ,@freq_subday_type ,@freq_subday_interval ,@freq_relative_interval ,@freq_recurrence_factor ,@active_start_date ,@active_end_date ,@active_start_time ,@active_end_time ,@owner_login_name ,@Action WHILE @@FETCH_STATUS = 0 BEGIN if @Action = 'INSERT' BEGIN exec msdb.dbo.sp_add_schedule @schedule_name ,@enabled ,@freq_type ,@freq_interval ,@freq_subday_type ,@freq_subday_interval ,@freq_relative_interval ,@freq_recurrence_factor ,@active_start_date ,@active_end_date ,@active_start_time ,@active_end_time ,@owner_login_name END IF @Action = 'UPDATE' BEGIN exec msdb.dbo.sp_update_schedule @schedule_id = @schedule_id ,@new_name = @schedule_name ,@enabled = @enabled ,@freq_type = @freq_type ,@freq_interval = @freq_interval ,@freq_subday_type = @freq_subday_type ,@freq_subday_interval = @freq_subday_interval ,@freq_relative_interval = @freq_relative_interval ,@freq_recurrence_factor = @freq_recurrence_factor ,@active_start_date = @active_start_date ,@active_end_date = @active_end_date ,@active_start_time = @active_start_time ,@active_end_time = @active_end_time ,@owner_login_name = @owner_login_name END FETCH NEXT FROM db_cursor INTO @Schedule_id ,@schedule_name ,@enabled ,@freq_type ,@freq_interval ,@freq_subday_type ,@freq_subday_interval ,@freq_relative_interval ,@freq_recurrence_factor ,@active_start_date ,@active_end_date ,@active_start_time ,@active_end_time ,@owner_login_name ,@Action END CLOSE db_cursor DEALLOCATE db_cursor drop table #Schedules END
The code is a bit longer than categories, but that is just because there are more fields to copy. It is just as simple as last time. Open a cursor, select all of the data that needs to be inserted or updated into fields, decide if the action is INSERT or UPDATE and then call the stored proc to insert or update the data. Finally, get the next record from the cursor.
That’s it! Schedules are now being replicated between SQL instances in the AAG. Next up is how to replicate the actual Job.