Help – DBMS_SCHEDULER keeps Spamming me…and can’t tell the time either

Sundays – a day of rest. Certainly true for me. Sunday morning is a time for lazing around leafing through the colour supplements and thinking about nothing in particular. Sunday 23rd October was a little bit different.
Wide-awake at 8 am ( I didn’t know that there was such a time as 8am on a Sunday), like several million others, I was wondering what would confront the All Blacks – the Gallic flair with which France had swept aside England or the Gallic shrug with which they had surrendered to Tonga ?
Look, I’m not really a New Zealander. Yes, I was born in Auckland but both my parents are English and I’ve lived most of my life in England. However, like anyone with a connection to the Land of the Long White Cloud, there is a part of my soul, however small, that takes the form of a Rugby ball.
At the end of the match, I was able to join my “fellow” Kiwis in, not so much paroxysms of joy as a huge collective sigh of relief.

On the whole though, I’d rather not have to see Sunday morning from that early on. So, if there is, for example, something that needs to run on my database on a Sunday morning, I’d rather the database just did it without my intervention.

What I plan to do here is :

  1. set up a scheduler job
  2. explore the ways in which we can control whether a class of job runs on a given database
  3. stop jobs running on database startup
  4. teach the scheduler how to tell the time – especially in terms of daylight saving

Creating a scheduler job

There is a really good and comprehensive guide on the myriad options available for Oracle’s database scheduler over at Oracle Base.

Here, I’m going to keep things quick and simple.
I’ve got a table that looks something like this :

CREATE TABLE reminders(
    message VARCHAR2(4000),
    msg_ts TIMESTAMP)
/

For some reason I can't think of right now, I need to insert a row into this table at 8.30 every Sunday morning.
Rather than me having to drag my weary bones all the way to the keyboard, I can simply get the database to remember to do it. So, as a user with CREATE JOB privilege :

DECLARE
    l_job_action VARCHAR2(500) := 
        'BEGIN '
            ||'INSERT INTO reminders( message, msg_ts) '
            ||'VALUES(''What time do you call this ?'', SYSTIMESTAMP); '
            ||'END;';
    
BEGIN
    DBMS_SCHEDULER.CREATE_JOB(
        job_name => 'wakeup_sun',
        job_type => 'PLSQL_BLOCK',
        job_action => l_job_action,
        start_date => SYSDATE,
        repeat_interval => 'freq=weekly; byday=SUN; byhour=8; byminute=30',
        end_date => NULL,
        enabled => TRUE,
        comments => 'Do something automatically on Sunday morning');
END;
/

Notice anything odd about the START_TIME parameter ? Neither did I...at first. We'll come back to this a little later. In the meantime, if we want to test that our job will actually do what we need it to, we can do a test run :

BEGIN
    DBMS_SCHEDULER.RUN_JOB(job_name => 'wakeup_sun');
END;
/

PL/SQL procedure successfully completed.

SQL> SELECT * 
  2  FROM reminders;

MESSAGE
------------------------------
MSG_TS
---------------------------------------------------------------------------
What time do you call this ?
10-NOV-11 20.16.09.044987

We could even tidy things up a bit by creating a stored procedure to do the work and then just calling that from the scheduler job.

CREATE OR REPLACE PROCEDURE sunday_am_pr IS
BEGIN
    INSERT INTO reminders( message, msg_ts)
    VALUES( 'Now we know that DBA stands for Does Bugger All !', SYSTIMESTAMP);
END sunday_am_pr;
/

And now the job to call it …

BEGIN
    DBMS_SCHEDULER.CREATE_JOB(
        job_name => 'sunday_am',
        job_type => 'STORED_PROCEDURE',
        job_action => 'sunday_am_pr',
        start_date => SYSDATE,
        repeat_interval => 'freq=weekly; byday=SUN; byhour=8; byminute=30',
        end_date => NULL,
        enabled => TRUE,
        comments => 'Do something automatically on Sunday morning');
END;
/

The scheduler can't tell the time

Let's have a look at those jobs we just created in a little more detail :

SQL> SELECT TO_CHAR(start_date, 'DD-MON-RR HH24:MI:SS TZR')
  2  FROM dba_scheduler_jobs
  3  WHERE owner = USER
  4  AND job_name = 'SUNDAY_AM';

TO_CHAR(START_DATE,'DD-MON-RRHH24:MI:SSTZR')
--------------------------------------------------------------------------------
10-NOV-11 20:22:55 +00:00

SQL> 

Hmmm, the start time doesn't seem to have an offset to account for daylight savings. So, rather than using SYSDATE as the START_TIME when defining a job, maybe we should be using SYSTIMESTAMP ?
Well, yes and no. SYSTIMESTAMP will work fine for jobs that have run intervals of less than 24 hours. However, for jobs such as this one, it will blithely ignore changes in daylight saving. So, when, in my case, we move from the optimistically named British Summer Time ( BST) back to GMT, the job will run an hour later than before.

If we're going to get the job to run at the correct time, we need to be explicit.
Just in case you've been typing along whilst reading this post and are now busy cursing me as you'll have to go back and drop and re-create any jobs you've already set up, this script should set things right :


set serveroutput on
DECLARE
    CURSOR c_jobs IS
        SELECT job_name, start_date
        FROM dba_scheduler_jobs
        WHERE owner = USER;
BEGIN
    FOR r_jobs IN c_jobs LOOP
        DBMS_SCHEDULER.SET_ATTRIBUTE(
            name => r_jobs.job_name,
            attribute => 'start_date',
            value => TO_TIMESTAMP_TZ(
                TO_CHAR(r_jobs.start_date, 'DDMMRR HH24:MI:SS')
                    ||'EUROPE/LONDON', 'DDMMRR HH24:MI:SS TZR'));
        DBMS_OUTPUT.PUT_LINE('Job '||r_jobs.job_name||' altered.');
    END LOOP;
END;
/

After running this script, if we re-run the same query we get...

SQL> select TO_CHAR(start_date, 'DD-MON-RR HH24:MI:SS TZR')
  2  FROM dba_scheduler_jobs
  3  WHERE job_name = 'SUNDAY_AM'
  4  /

TO_CHAR(START_DATE,'DD-MON-RRHH24:MI:SSTZR')
--------------------------------------------------------------------------------
10-NOV-11 20:22:55 EUROPE/LONDON

Help, my database is spamming me

So, you have your scheduled jobs running happily in the database. You probably have a few sending e-mails to you as the DBA to let you know all is well, or if there is a problem.
What happens when you refresh your test database with a copy of Live ?

Yep, as soon as you open the instance, the scheduler checks to see if any jobs should have run. If the current system time is later than the next_run_time, then they'll kick off immediately. Even if the first thing you do after opening the database is to disable these jobs, your inbox is still going to be flooded with spam.

In the good old days of dbms_job, you could avoid this simply by setting the job_queue_processes parameter to 0. This solution has now finally found it's way into 11gR2 as well. However, if you're currently stuck somewhere between 10g and 11gR1 then you'll need an alternative.
The workaround below is a quick n dirty version of this infinitely more elegant solution proposed by Dan Morgan.

Getting classy

The first thing we need to do is create a service. For our specific purposes, you can think of a service as....well...a thingy.

BEGIN
    DBMS_SERVICE.CREATE_SERVICE( 
        service_name => 'not_so_fast', 
        network_name => 'some_random_string');
END;
/

Next, we create a job class and associate it with the service...

BEGIN
    DBMS_SCHEDULER.CREATE_JOB_CLASS(
        job_class_name => 'only_on_live',
        service => 'not_so_fast');
END;
/

Finally, we assign our job to the job class

set serveroutput on
DECLARE
    CURSOR c_jobs IS
        SELECT job_name
        FROM dba_scheduler_jobs
        WHERE owner = USER;
BEGIN
    FOR r_jobs IN c_jobs LOOP
        DBMS_SCHEDULER.SET_ATTRIBUTE(
            name => r_jobs.job_name,
            attribute => 'job_class',
            value => 'only_on_live');
        DBMS_OUTPUT.PUT_LINE('Job '||r_jobs.job_name||' added to ONLY_ON_LIVE class.');
    END LOOP;
END;
/

NOTE – if you subsequently create any jobs that are assigned to a class owned by someone else then you need to GRANT EXECUTE ANY CLASS to the job owner. Otherwise, when you run the job you’ll get :

ORA-27486: insufficient privileges
ORA-06512: at "SYS.DBMS_ISCHED", line 2751
ORA-06512: at "SYS.DBMS_SCHEDULER", line 1794
ORA-06512: at line 2

This happens even if you’re trying to run the job as sys as sysdba.

In order to enable or disable jobs in a specified class, all we need to do is enable or disable the service with which the class is associated.


BEGIN
    DBMS_SERVICE.START_SERVICE( service_name => 'not_so_fast');
END;
/

You can see what services are running on your database by querying v$active_services...

SQL> SELECT name
  2  FROM v$active_services 
  3  /

NAME
------------------------------
not_so_fast
XEXDB
XE
SYS$BACKGROUND
SYS$USERS

If the service is not running, then none of the jobs associated with the class that is, in turn, associated with the service, will run.
So how do we stop the spam ? Dan's solution used a simple config file based on the same format as oratab. Mine is a bit more basic, but does have the advantage of meaning that you have one less thing to remember when cloning your database.
Here, we're going to use a database startup trigger :

CREATE OR REPLACE TRIGGER start_service_trg
    AFTER STARTUP ON DATABASE 
DECLARE
    lc_live_name CONSTANT v$database.name%TYPE := 'XE';
    l_db_name v$database.name%TYPE;
BEGIN
    SELECT name INTO l_db_name FROM v$database;
    IF l_db_name = lc_live_name THEN
        DBMS_SERVICE.START_SERVICE( service_name => 'not_so_fast');
    END IF;
END;
/

Simple really, only start the service if we're running on production. When I start a test database (in this case, where the database name is not XE, the trigger will not enable the services ( and therefore the jobs) thus keeping my inbox spam free.
Regular readers may be wondering how I got through this entire explanation without any Monty Python references.

Take it easy, I'm just recovering

What about those times when you're bringing the production database back up after some unforeseen downtime. The last thing you want is a load of potentially long-running jobs to kick off and hog all of the available database resources.
If you haven't used the Services method above, you're going to have to find an alternative solution...

STARTUP NOMOUNT
ALTER SYSTEM ENABLE RESTRICTED SESSION;

Once you open the database, restricted session is still enabled and the jobs don't run.
You can then disable them at your leisure before...

ALTER SYSTEM DISABLE RESTRICTED SESSION;

Once you open the database, restricted session is still enabled and the jobs don't run.
You can then disable them at your leisure before...

ALTER SYSTEM DISABLE RESTRICTED SESSION;

Deb has been sulking most of the way through this post. In fact, she's not been entirely happy ever since the French knocked out Wales in the Semis. Oh well, only another 4 years till the next World Cup. I mean, how long can a sulk last ?

About these ads

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s