Differenze
Queste sono le differenze tra la revisione selezionata e la versione attuale della pagina.
| Prossima revisione | Revisione precedente | ||
| hobby:development:sql:database_scheduler [2026/08/14 10:31] – creata mauro.cortese | hobby:development:sql:database_scheduler [2026/08/14 14:00] (versione attuale) – mauro.cortese | ||
|---|---|---|---|
| Linea 5: | Linea 5: | ||
| \\ | \\ | ||
| + | Scheduler table-driven in SQL Server, basato su SQL Server Agent con una tabella di configurazione e una stored procedure " | ||
| + | === Architettura === | ||
| + | - **Tabella di configurazione: | ||
| + | - **Tabella di log:** traccia le esecuzioni (successo/ | ||
| + | - **Stored procedure dispatcher: | ||
| + | - **Un solo SQL Server Agent Job:** che gira ogni minuto (o ogni X minuti) e chiama il dispatcher | ||
| + | Vantaggio: non serve creare un job Agent per ogni procedura, ne basta uno solo che orchestra tutto in base alla tabella. | ||
| + | |||
| + | |||
| + | === Creazione delle tabelle === | ||
| + | <sxh sql> | ||
| + | -- ----------------------------------------------------------------------------- | ||
| + | -- 1) CONFIGURATION TABLE | ||
| + | -- Stores the definition of every job. ScheduleType drives which of the other | ||
| + | -- scheduling columns/ | ||
| + | -- ----------------------------------------------------------------------------- | ||
| + | CREATE TABLE dbo.scheduler_config( | ||
| + | IdJob INT IDENTITY(1, | ||
| + | JobName | ||
| + | ProcedureSchema | ||
| + | ProcedureName | ||
| + | Parameters | ||
| + | ScheduleType | ||
| + | -- DAILY_TIME -> runs at specific times of day, listed in scheduler_config_times | ||
| + | -- ONE_TIME | ||
| + | FrequencyMinutes | ||
| + | SpecificDateTime | ||
| + | StartTime | ||
| + | EndTime | ||
| + | WeekDays | ||
| + | IsActive | ||
| + | IsRunning | ||
| + | LastRunDate | ||
| + | | ||
| + | CONSTRAINT sK_Schedu_cerConfig_ScheduleType | ||
| + | CHECK (ScheduleType IN (' | ||
| + | ); | ||
| + | GO | ||
| + | |||
| + | -- ----------------------------------------------------------------------------------- | ||
| + | -- 2) FIXED DAILY RUN TIMES | ||
| + | -- Holds one or more fixed times of day for jobs whose ScheduleType = ' | ||
| + | -- Example: a job can run at both 08:00 and 18:00 by inserting two rows here. | ||
| + | -- ----------------------------------------------------------------------------------- | ||
| + | CREATE TABLE dbo.scheduler_config_times ( | ||
| + | Id INT IDENTITY(1, | ||
| + | ConfigID | ||
| + | RunTime | ||
| + | LastRunDate | ||
| + | ); | ||
| + | GO | ||
| + | |||
| + | -- ---------------------------------------------------------------------------- | ||
| + | -- 3) EXECUTION LOG | ||
| + | -- Keeps an execution history for every run of every job, including duration | ||
| + | -- and any error raised. | ||
| + | -- ---------------------------------------------------------------------------- | ||
| + | CREATE TABLE dbo.scheduler_log ( | ||
| + | ID INT IDENTITY(1, | ||
| + | ConfigID | ||
| + | ProcedureName | ||
| + | StartDate | ||
| + | EndDate | ||
| + | Outcome | ||
| + | ErrorMessage | ||
| + | ); | ||
| + | GO | ||
| + | |||
| + | -- ----------------------------------------------------------------------------- | ||
| + | -- 4) SUPPORTING INDEXES | ||
| + | -- The dispatcher filters on ScheduleType / IsActive / IsRunning on every run | ||
| + | -- (every minute), so these columns need a covering index to avoid a table | ||
| + | -- scan as scheduler_config grows. | ||
| + | -- ----------------------------------------------------------------------------- | ||
| + | CREATE NONCLUSTERED INDEX sX_Schedu_cerConfig_Dispatch | ||
| + | ON dbo.scheduler_config (ScheduleType, | ||
| + | INCLUDE (ProcedureSchema, | ||
| + | GO | ||
| + | | ||
| + | CREATE NONCLUSTERED INDEX sX_Schedu_cerConfigTimes_ConfigID | ||
| + | ON dbo.scheduler_config_times (ConfigID) | ||
| + | INCLUDE (RunTime, LastRunDate); | ||
| + | GO | ||
| + | |||
| + | </ | ||
| + | |||
| + | === Dispatcher procedure === | ||
| + | |||
| + | <sxh sql> | ||
| + | |||
| + | -- ---------------------------------------------------------------------------- | ||
| + | -- DISPATCHER PROCEDURE | ||
| + | -- Main orchestrator, | ||
| + | -- Agent Job. Handles INTERVAL, DAILY_TIME and ONE_TIME scheduling. | ||
| + | -- No CURSOR object is used: due jobs are collected in a table variable and | ||
| + | -- walked with a WHILE loop, which is required only because each dynamic | ||
| + | -- EXEC needs its own isolated TRY/CATCH so one failing job never blocks | ||
| + | -- the others. All status/log updates that don't need per-row isolation | ||
| + | -- are done as set-based statements instead. | ||
| + | -- -------------------------------------------------------------------------- | ||
| + | -- SAMPLE CONFIGURATION ROWS | ||
| + | -- -------------------------------------------------------------------------- */ | ||
| + | -- INSERT INTO dbo.scheduler_config (JobName, ProcedureName, | ||
| + | -- VALUES (' | ||
| + | |||
| + | -- DECLARE @NewConfigID INT; | ||
| + | -- INSERT INTO dbo.scheduler_config (JobName, ProcedureName, | ||
| + | -- VALUES (' | ||
| + | -- SET @NewConfigID = SCOPE_IDENTITY(); | ||
| + | -- INSERT INTO dbo.scheduler_config_times (ConfigID, RunTime) | ||
| + | -- VALUES (@NewConfigID, | ||
| + | |||
| + | -- INSERT INTO dbo.scheduler_config (JobName, ProcedureName, | ||
| + | -- VALUES (' | ||
| + | -- ---------------------------------------------------------------------------- | ||
| + | CREATE OR ALTER PROCEDURE dbo.sp_scheduler_dispatcher | ||
| + | AS | ||
| + | BEGIN | ||
| + | SET NOCOUNT ON; | ||
| + | |||
| + | DECLARE @Now DATETIME | ||
| + | DECLARE @Today DATE = CAST(@Now AS DATE); | ||
| + | DECLARE @NowTime TIME = CAST(@Now AS TIME); | ||
| + | DECLARE @TodayWeekDay VARCHAR(1) = CAST(DATEPART(WEEKDAY, | ||
| + | |||
| + | -- Working table with a row number, used to walk through the due jobs one | ||
| + | -- at a time without allocating a CURSOR object. LogID is filled later via | ||
| + | -- an OUTPUT clause, avoiding a lookup query inside the loop. | ||
| + | DECLARE @DueJobs TABLE ( | ||
| + | RowNum | ||
| + | ConfigID | ||
| + | ProcedureSchema | ||
| + | ProcedureName | ||
| + | Parameters | ||
| + | TimeSlotID | ||
| + | LogID INT NULL | ||
| + | ); | ||
| + | |||
| + | -- 1) INTERVAL jobs: due when enough minutes have passed since LastRunDate, | ||
| + | -- and (if set) we're inside the allowed time window and weekday list. | ||
| + | INSERT INTO @DueJobs (ConfigID, ProcedureSchema, | ||
| + | SELECT ID, ProcedureSchema, | ||
| + | FROM dbo.scheduler_config | ||
| + | WHERE IsActive = 1 | ||
| + | AND IsRunning = 0 | ||
| + | AND ScheduleType = ' | ||
| + | AND (LastRunDate IS NULL OR DATEDIFF(MINUTE, | ||
| + | AND (StartTime IS NULL OR @NowTime >= StartTime) | ||
| + | AND (EndTime IS NULL OR @NowTime <= EndTime) | ||
| + | AND (WeekDays IS NULL OR WeekDays LIKE ' | ||
| + | |||
| + | -- 2) DAILY_TIME jobs: due when current time has reached a configured | ||
| + | -- RunTime slot and that slot hasn't already fired today. | ||
| + | INSERT INTO @DueJobs (ConfigID, ProcedureSchema, | ||
| + | SELECT c.ID, c.ProcedureSchema, | ||
| + | FROM dbo.scheduler_config c | ||
| + | INNER JOIN dbo.scheduler_config_times t ON t.ConfigID = c.ID | ||
| + | WHERE c.IsActive = 1 | ||
| + | AND c.IsRunning = 0 | ||
| + | AND c.ScheduleType = ' | ||
| + | AND (c.WeekDays IS NULL OR c.WeekDays LIKE ' | ||
| + | AND (t.LastRunDate IS NULL OR t.LastRunDate < @Today) | ||
| + | AND CONVERT(CHAR(5), | ||
| + | |||
| + | -- 3) ONE_TIME jobs: due once, when current datetime has reached | ||
| + | -- SpecificDateTime and the job has never run before. | ||
| + | INSERT INTO @DueJobs (ConfigID, ProcedureSchema, | ||
| + | SELECT ID, ProcedureSchema, | ||
| + | FROM dbo.scheduler_config | ||
| + | WHERE IsActive = 1 | ||
| + | AND IsRunning = 0 | ||
| + | AND ScheduleType = ' | ||
| + | AND LastRunDate IS NULL | ||
| + | AND @Now >= SpecificDateTime; | ||
| + | |||
| + | -- Nothing due: exit early, no further statements needed. | ||
| + | IF NOT EXISTS (SELECT * FROM @DueJobs) | ||
| + | RETURN; | ||
| + | |||
| + | -- Mark every due job as running and stamp LastRunDate in one set-based UPDATE, | ||
| + | -- instead of doing it row-by-row inside the loop. | ||
| + | UPDATE c SET | ||
| + | | ||
| + | , | ||
| + | FROM dbo.scheduler_config c | ||
| + | INNER JOIN @DueJobs d ON d.ConfigID = c.ID; | ||
| + | |||
| + | -- Same for DAILY_TIME slots: stamp all fired slots at once. | ||
| + | UPDATE t SET | ||
| + | t.LastRunDate = @Today | ||
| + | FROM dbo.scheduler_config_times t | ||
| + | INNER JOIN @DueJobs d ON d.TimeSlotID = t.ID | ||
| + | WHERE d.TimeSlotID IS NOT NULL; | ||
| + | |||
| + | -- Bulk-insert one ' | ||
| + | -- IDs directly via OUTPUT so the loop below needs no lookup query. | ||
| + | DECLARE @LogMap TABLE ( | ||
| + | | ||
| + | ,LogID INT | ||
| + | ); | ||
| + | |||
| + | INSERT INTO dbo.scheduler_log (ConfigID, ProcedureName, | ||
| + | | ||
| + | | ||
| + | |||
| + | UPDATE d SET | ||
| + | d.LogID = m.LogID | ||
| + | FROM @DueJobs d | ||
| + | INNER JOIN @LogMap m ON m.ConfigID = d.ConfigID; | ||
| + | |||
| + | -- Execute each due procedure individually: | ||
| + | -- handling genuinely requires row-by-row processing, so this is a plain | ||
| + | -- WHILE loop keyed on RowNum rather than a CURSOR. | ||
| + | DECLARE @i INT = 1; | ||
| + | DECLARE @Count INT = (SELECT COUNT(*) FROM @DueJobs); | ||
| + | DECLARE @ConfigID INT; | ||
| + | DECLARE @Schema SYSNAME; | ||
| + | DECLARE @Name SYSNAME; | ||
| + | DECLARE @Params NVARCHAR(MAX) | ||
| + | DECLARE @LogID INT; | ||
| + | DECLARE @SqlCmd NVARCHAR(MAX); | ||
| + | |||
| + | WHILE @i <= @Count | ||
| + | BEGIN | ||
| + | SELECT | ||
| + | @ConfigID = ConfigID, | ||
| + | @Schema | ||
| + | @Name = ProcedureName, | ||
| + | @Params | ||
| + | @LogID | ||
| + | FROM @DueJobs | ||
| + | WHERE RowNum = @i; | ||
| + | |||
| + | -- QUOTENAME() protects schema/ | ||
| + | -- reserved-word issues; parameters, if present, are appended as-is. | ||
| + | SET @SqlCmd = QUOTENAME(@Schema) + ' | ||
| + | |||
| + | BEGIN TRY | ||
| + | EXEC (@SqlCmd); | ||
| + | UPDATE dbo.scheduler_log SET EndDate = GETDATE(), Outcome = ' | ||
| + | END TRY | ||
| + | BEGIN CATCH | ||
| + | -- One failing procedure never stops the loop: error is logged, | ||
| + | -- next job proceeds. | ||
| + | UPDATE dbo.scheduler_log | ||
| + | SET EndDate = GETDATE(), Outcome = ' | ||
| + | WHERE ID = @LogID; | ||
| + | END CATCH | ||
| + | |||
| + | -- Always release the running flag, whether the procedure succeeded or failed. | ||
| + | UPDATE dbo.scheduler_config SET IsRunning = 0 WHERE ID = @ConfigID; | ||
| + | |||
| + | SET @i += 1; | ||
| + | END | ||
| + | END | ||
| + | GO | ||
| + | </ | ||