I am trying to work on altering an already existing queuing system... Which just gives me a headache...
The logic behind the queues is all in Oracle DB as follows:
Database #1 sends notifications, which are queued in Database #2 in a specific queue, let's name it Q1.
There is a regular job, that executes a procedure in a package (DISPATCHER), which basically is a loop that dequeues all messages in Q1 until no messages are there. Its execution is scheduled every 5 minutes, yet with the amount of queue entries it runs longer than that.
Dispatcher procedure distributes the messages to additional queues, from which Java processes the messages and things go further.
There isn't much control over the dispatching JOB so I decided to look up ways of making the whole framework for dispatching jobs better. I must add it is an old implementation that worked for quite a while, but most don't find it easy to control or transparent.
My point is if there are any secure ways of this dispatching process?
I have created a trigger on the queue table that executes a single dequeue with all the processing of the message done in DISPATCHER and it works, but i don't know if it's best the idea.
Looked up creating subscribers, but can't add any as it's a single-consumer queue. Considering i would have to update the framework, recreating the queue would delete all the messages that already are in the queue, which is something i'd like to omit...
Tried a DBMS Scheduler job to execute the DISPATCHER for more control, but it just runs "when it has resources". I'm counting on a solution that works continuously and processes the messages as fast as it can.
Maybe there is some more elaborate way to solve this problem, but i'm still a bit fresh and can't seem to grasp the whole advanced queuing mechanism...
Thanks in advance.
Considering constantly running job it's possible to control procedure execution by sending message through DBMS_PIPE.
Control code sends stop message through dbms_pipe.
Dispatcher code runs constantly in DBMS_SCHEDULER job and checks if a stop message arrived in pipe.
Also it's possible to force an interrupt of DBMS_SHCEDULER job after some timeout.
Years ago I had implemented such a logic and this solution runs till now. Code below is an adapted sample which can help you to check if this approach acceptable and useful for your case.
Control package header:
create or replace package DispatcherControl is
-- Package to control job state
procedure StartDispatcher;
procedure StopDispatcher;
end;
Control package body:
create or replace package body DispatcherControl is
-- User who owns dispatcher job in DBMS_SCHEDULER
C_DISPATCH_PROGRAM_OWNER constant varchar2(100) := 'DISPATCH_USER';
-- Main procedure to handle dispatcher requests
C_DISPATCH_JOB_PROC constant varchar2(100) := 'DISPATCH_USER.DISPATCHER.MAINPROC';
-- Name for dispatch program
C_DISPATCH_PROGRAM_NAME constant varchar2(100) := 'DISPATCH_PROGRAM';
-- Description for dispatch program
C_DISPATCH_PROGRAM_COMMENT constant varchar2(100) := 'Q1 dispatch task';
-- Dispatcher job name.
C_DISPATCH_JOB_NAME constant varchar2(100) := 'DISPATCH_JOB';
-- Description for dispatcher job
C_DISPATCH_JOB_COMMENT constant varchar2(100) := 'Q1 dispatch process';
-- Pipe name for dispatch job control.
C_DISPATCH_PIPE_NAME constant varchar2(100) := 'DISPATCH_CONTROL';
-- Message text for pipe to stop dispatch job
C_PIPE_STOP_MSG constant varchar2(100) := 'stop_dispatch';
-- Check if DBMS_SCHEDULER program exists and create it if needed
procedure CheckDispatcherJobExists
is
begin
-- Return if program exists
for cDummy in (
select 1
from all_scheduler_programs
where
owner = C_DISPATCH_PROGRAM_OWNER and program_name = C_DISPATCH_PROGRAM_NAME
and
rownum = 1
) loop
return;
end loop;
-- Create disabled if not found
sys.dbms_scheduler.create_program(
program_name => C_DISPATCH_PROGRAM_OWNER || '.' || C_DISPATCH_PROGRAM_NAME,
program_type => 'STORED_PROCEDURE',
program_action => C_DISPATCH_JOB_PROC,
number_of_arguments => 0,
enabled => false,
comments => C_DISPATCH_PROGRAM_COMMENT
);
-- Enable program
sys.dbms_scheduler.enable(C_DISPATCH_PROGRAM_OWNER || '.' || C_DISPATCH_PROGRAM_NAME);
end;
-- Check status of dispatcher job and run it if not found
procedure CheckDispatcherJobState
is
begin
CheckDIspatcherJobExists;
-- Check if job is currently running according to scheduler info
for cDummy in (
select * from all_scheduler_jobs
where
owner = C_DISPATCH_PROGRAM_OWNER
and
job_name = C_DISPATCH_JOB_NAME
) loop
-- Job found, check if running
for cDummy2 in (
select * from all_scheduler_running_jobs
where
owner = C_CIS_PROGRAM_OWNER and job_name = C_DISPATCH_JOB_NAME
) loop
-- Check if job really running by checking sessions list
for cDummy3 in (
select 1
from
all_scheduler_running_jobs jobs,
sys.v_$session sessions
where
jobs.owner = C_DISPATCH_PROGRAM_OWNER and jobs.job_name = C_DISPATCH_JOB_NAME
and
sessions.sid = jobs.session_id and sessions.process = jobs.slave_os_process_id
and
sessions.action = jobs.job_name and sessions.username = jobs.owner
and
sessions.module = 'DBMS_SCHEDULER'
) loop
-- Ok, return
return;
end loop;
-- No process found for running job, stop and delete task before recreation
sys.dbms_scheduler.stop_job(C_DISPATCH_JOB_NAME, true);
end loop;
-- process found, but restart needed
sys.dbms_scheduler.drop_job(C_DISPATCH_JOB_NAME, true);
end loop;
-- Create one-time running job with manual start
sys.dbms_scheduler.create_job(
job_name => C_DISPATCH_JOB_NAME,
program_name => C_DISPATCH_PROGRAM_OWNER || '.' || C_DISPATCH_PROGRAM_NAME,
enabled => true,
auto_drop => true,
start_date => add_months(sysdate,1000),
repeat_interval => null,
end_date => null,
comments => C_DISPATCH_JOB_COMMENT
);
-- Run created task
sys.dbms_scheduler.run_job(
job_name => C_DISPATCH_JOB_NAME,
use_current_session => false
);
end;
-- Stop and drop dispatch job.
procedure DropDispatchJob
is
vSendRC integer;
begin
-- Send request through DBMS_PIPE and wait for timeout.
sys.dbms_pipe.reset_buffer;
sys.dbms_pipe.pack_message(C_PIPE_STOP_MSG);
vSendRC := sys.dbms_pipe.send_message(C_DISPATCH_PIPE_NAME,1);
if(vSendRC = 0) then
-- wait if sent Ok
sys.dbms_lock.sleep(0.25);
end if;
-- force job stop (not done in case of drop_job)
for cDummy in (
select 1 from all_scheduler_running_jobs
where
owner = C_DISPATCH_PROGRAM_OWNER
and
job_name = C_DISPATCH_JOB_NAME
) loop
begin
sys.dbms_scheduler.stop_job(C_DISPATCH_JOB_NAME, true);
exception
when others then begin
-- If sqlcode = -27366 then task finished, in other case it's unexpected
if(SQLCODE != -27366) then
raise;
end if;
end;
end;
end loop;
-- delete job if exists, allow stopping
for cDummy in (
select 1 from all_scheduler_jobs
where
owner = C_DISPATCH_PROGRAM_OWNER
and
job_name = C_DISPATCH_JOB_NAME
) loop
sys.dbms_scheduler.drop_job(C_DISPATCH_JOB_NAME, true);
end loop;
-- drop program if exists
for cDummy in (
select 1 from all_scheduler_programs
where
owner = C_DISPATCH_PROGRAM_OWNER
and
program_name = C_DISPATCH_PROGRAM_NAME
) loop
sys.dbms_scheduler.drop_program(C_DISPATCH_PROGRAM_NAME, true);
end loop;
-- Clear DBMS_PIPE messages after job stop (cover case of forced stop).
sys.dbms_pipe.purge(C_DISPATCH_PIPE_NAME);
end;
-- Start dispatcher process
procedure StartDispatcher
is
begin
-- Clear message queue before starting
sys.dbms_pipe.purge(C_DISPATCH_PIPE_NAME);
CheckDispatchJobState;
end;
-- Stop dispatcher process
procedure StopDispatcher
is
begin
DropDispatchJob;
end;
end;
Dispatcher package header:
create or replace package DISPATCH_USER.Dispatcher is
-- Main queue check procedure to run from job.
procedure MainProc;
end;
Dispatcher package body:
create or replace package body DISPATCH_USER.Dispatcher is
-- Normal wait time in seconds
NORMAL_CHECK_INTERVAL constant number := 0.01;
-- Checks if stop message received.
function CheckIsStopped return boolean
is
vPipeRC integer;
vMsg varchar2(1024);
begin
-- check if message exists
vPipeRC := sys.dbms_pipe.receive_message('DISPATCH_CONTROL',0);
if(vPipeRC = 0) then
-- check type of message content, must be varchar2 (look for constants in sys.dbms_pipe package).
if(sys.dbms_pipe.next_item_type = 9) then
-- check message content
sys.dbms_pipe.unpack_message(vMsg);
if(vMsg = 'stop') then
return true;
end if;
end if;
end if;
return false;
end;
-- Checks if error caused by external interrupt
function IsInterruptError(piSQLCode in number) return boolean
is
begin
if( piSQLCode in (
-1013, -- ORA-01013: User requested cancel of current operation
-28, -- ORA-00028: Your session has been killed
-13638, -- ORA-13638: The user interrupted the current operation
-13639, -- ORA-13639: The current operation was interrupted because it timed out.
-13668, -- ORA-13668: The current operation was aborted because it was blocking another session
-48223, -- ORA-48223: Interrupt Requested - Fetch Aborted - Return Code [string] [string]
-48495 -- ORA-48495: Interrupt requested
)
) then
return true;
end if;
return false;
end;
-- Main procedure for dispatcher job
procedure MainProc
is
vIsFound boolean;
vSQLCode number;
vErrMsg varchar2(2048);
vCheckInterval number;
vMESSAGE SOME_CUSTOM_MESSAGE_DATA_TYPE; -- Just for example
begin
vCheckInterval := NORMAL_CHECK_INTERVAL;
while(true) loop
vIsFound := false;
begin
vMESSAGE := GET_NEXT_MESSAGE_FROM_QUEUE; -- Just for example
if(vMESSAGE is not null) then
vIsFound := true;
-- Process received message her
DISPATCH_MESSAGE(vMESSAGE); -- Just for example
-- Commit changes to save a redo log from overflow.
commit;
end if;
exception
when others then begin
vSQLCode := SQLCODE;
vErrMsg := SQLERRM;
if( IsInterruptError(vSQLCode) ) then
-- Promote error if interrupted forcibly
raise;
end if;
-- In other cases just write error conditions to log and continue.
-- Log writing totally skipped from this example, so only comment here.
-- Also it's a place to perform extra error analysis.
-- E.g. if it is some temporary error caused by remote database shutdown
-- and so on, then increase vCheckInterval and don't stress server.
end;
end;
-- Check if interrupted programmatically from control procedure
if( CheckIsStopped ) then
-- normal exit, job finished
return;
end if;
-- If there are no new request then put process in sleep state
-- for a short time to release resources
if(not vIsFound) then
dbms_lock.sleep(vCheckInterval);
end if;
end loop;
-- This point never reached
null;
end;
end;
Please note, that code above is a simplified example (e.g. no logging of performed activity). Also, example code adopted to match question conditions and contain some assumptions like Oracle user names and generics like GET_NEXT_MESSAGE_FROM_QUEUE. Please feel free to ask if something not clear in example code.
Related
I'm using the following code to cancel a sales order header using API OE_ORDER_PUB
But always returned Validation failed for the field - Change Reason
DECLARE
--variable declare
BEGIN
-- Setting the Enviroment --
FOR I
IN (SELECT oh.header_id order_header_id
FROM oe_order_headers_all oh
WHERE oh.order_number IN (12301000013) AND oh.org_id = v_org_id)
LOOP
BEGIN
-- CANCEL HEADER --
v_header_rec.cancelled_flag := 'Y';
v_header_rec.change_reason := 'Not Provided';
DBMS_OUTPUT.put_line ('Starting cancel of API');
-- CALLING THE API TO CANCEL AN ORDER --
OE_ORDER_PUB.PROCESS_ORDER (
x_header_rec => v_header_rec_out
--... using the parameter
);
END LOOP;
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line ('error in block. reason is :' || SQLERRM);
END;
How could i find the requirement rules for change reason field in OE_ORDER_HEADERS ?
There is not much info here to work with but here is what you could try
Check if there are validations on the page. The most probably cause of this error is that there is a page item with the label "Cancel Reason" that has a validation on it. That validation is failing which raises this validation error
Check if the procedure OE_ORDER_PUB.PROCESS_ORDER has any code to verify if the order can be cancelled.
Also, DBMS_OUTPUT.put_line cannot be used in apex pl/sql code. DBMS_OUTPUT prints message to the buffer of the client (sqlplus/sqldeveloper/apex sql workshop). In apex those calls will not do anything but they should be removed from the code since they could cause buffer overflows.
To properly instrument your code, use APEX_DEBUG to add comments that can be seen in debug mode and APEX_ERROR for handling errors.
For your code that would be something like
DECLARE
--variable declare
BEGIN
-- Setting the Enviroment --
FOR I
IN (SELECT oh.header_id order_header_id
FROM oe_order_headers_all oh
WHERE oh.order_number IN (12301000013) AND oh.org_id = v_org_id)
LOOP
BEGIN
-- CANCEL HEADER --
v_header_rec.cancelled_flag := 'Y';
v_header_rec.change_reason := 'Not Provided';
apex_debug.info('Starting cancel of API');
-- CALLING THE API TO CANCEL AN ORDER --
OE_ORDER_PUB.PROCESS_ORDER (
x_header_rec => v_header_rec_out
--... using the parameter
);
END LOOP;
EXCEPTION
WHEN OTHERS
THEN
apex_error.add_error (
p_message => 'error in block. reason is :' || SQLERRM,
p_display_location => apex_error.c_inline_in_notification );
END;
What I want is to execute a procedure ( that will run on Oracle 11g) concurrently, and to commit the whole operation if an only if all the concurrent transactions succeeded.
Two ways of parallel execution that I thought of were DBMS_PARALLEL_EXECUTE and dbms_job.submit(), but as I understand it, in both cases the processes created are running in their separate sessions and each process commits the changes upon termination (or in case of an error can rollback its own changes).
What I would like, is to start parallel processes, wait until each one of them is finished, check if they all were successful and only then commit the changes (or rollback if at least one process failed).
Is the above scenario possible? (And how can it be implemented)
Thanks.
I am curious why this requirement came about; I would probably question whether it was really necessary if this came to me. But if you cannot question the requirement, this is how I would go about it.
-- We need to have a common persistent location for all of the jobs to read
CREATE TABLE test_tab
(
job_name VARCHAR2(30),
status VARCHAR2(30)
)
/
-- The procedure writing to our table must be autonomous so that updates occur
-- without committing the rest of the work
CREATE OR REPLACE PROCEDURE test_log
(
i_job_name IN VARCHAR2,
i_status IN VARCHAR2
) IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
MERGE INTO test_tab tgt
USING dual
ON (tgt.job_name = i_job_name)
WHEN MATCHED THEN
UPDATE SET status = i_status
WHEN NOT MATCHED THEN
INSERT VALUES (i_job_name, i_status);
COMMIT;
END test_log;
/
CREATE OR REPLACE PROCEDURE test_proc(i_job_name IN VARCHAR2) IS
l_complete_cnt INTEGER;
l_error_cnt INTEGER;
l_waiting BOOLEAN := TRUE;
BEGIN
-- !!! Your code here !!!
/* -- Uncomment this block to prove the rollback scenario.
IF i_job_name LIKE '%8' THEN
raise_application_error(-20001, 'Throwing an error to prove rollback.');
END IF;*/
test_log(i_job_name, 'COMPLETE');
WHILE l_waiting LOOP
SELECT SUM(CASE WHEN status IN ('COMPLETE', 'COMMITTED') THEN 1 ELSE 0 END)
,SUM(CASE WHEN status = 'ERROR' THEN 1 ELSE 0 END)
INTO l_complete_cnt
,l_error_cnt
FROM test_tab
WHERE REGEXP_LIKE(job_name, 'TEST_JOB_\d');
IF l_complete_cnt = 8 THEN
COMMIT;
test_log(i_job_name, 'COMMITTED');
l_waiting := FALSE;
ELSIF l_error_cnt > 0 THEN
ROLLBACK;
test_log(i_job_name, 'ROLLBACK');
l_waiting := FALSE;
ELSE
dbms_lock.sleep(seconds => 5);
END IF;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
test_log(i_job_name, 'ERROR');
RAISE;
END;
/
-- Begin test section
BEGIN
FOR i IN 1..8 LOOP
dbms_scheduler.create_job('TEST_JOB_'||i
,'PLSQL_BLOCK'
,'BEGIN test_proc(''TEST_JOB_'||i||'''); END;'
,start_date => SYSDATE
,enabled => TRUE);
END LOOP;
END;
/
SELECT * FROM test_tab;
TRUNCATE TABLE test_tab; --the table should be cleared between tests
Using Oracle 11.2
Hi,
Here is what I want to do: I'm scheduling jobs using dbms_scheduler. The number of jobs to schedule is not fixed and a max of 4 jobs should run at the same time. The procedure scheduling the jobs should wait until all jobs are completed. If one job fails, the "schedule" procedure should also fail and all remaining scheduled jobs should be deleted from the scheduler.
Currently I have had to sleeping and polling the table user_scheduler_jobs in a loop.
I'm new to PL/SQL and rather inexperienced so please don't be too harsh on me ;)
Here is my code so far.
First the snippet for scheduling the jobs:
BEGIN
FOR r IN (SELECT p_values FROM some_table WHERE flag = 0 )
LOOP
-- count running jobs
SELECT count(*) INTO v_cnt
FROM user_scheduler_jobs
WHERE job_name LIKE 'something%';
/*
If max number of parallel jobs is reached, then wait before starting a new one.
*/
WHILE v_cnt >= l_max_parallel_jobs
LOOP
dbms_lock.sleep(10);
SELECT count(*) INTO v_cnt
FROM user_scheduler_jobs
WHERE job_name LIKE 'something%' AND state = 'RUNNING';
SELECT count(*) INTO v_cnt_failures
FROM user_scheduler_jobs
WHERE job_name LIKE 'something%' AND state = 'FAILED' OR state = 'BROKEN';
IF v_cnt_failures > 0 THEN RAISE some_exception; END IF;
END LOOP;
-- Start a new Job
v_job_name := 'something_someting_' || p_values;
v_job_action := 'begin user.some_procedure(''' || r.p_values || '''); end;';
dbms_scheduler.create_job(job_name => v_job_name,
job_type => 'PLSQL_BLOCK',
job_action => v_job_action,
comments => 'Some comment ' || v_job_name,
enabled => FALSE,
auto_drop => FALSE);
dbms_scheduler.set_attribute(NAME => v_job_name,
ATTRIBUTE => 'max_failures',
VALUE => '1');
dbms_scheduler.set_attribute(NAME => v_job_name,
ATTRIBUTE => 'max_runs',
VALUE => '1');
dbms_scheduler.enable(v_job_name);
v_job_count := v_job_count + 1;
-- array for all jobs
v_jobs_aat(v_job_count) := v_job_name;
END LOOP;
-- ... Wait till all jobs have finisched.
check_queue_completion(v_jobs_aat); -- see procedure below
END;
Procedure for waiting till last four jobs have finisched:
PROCEDURE check_queue_completion(p_jobs_aat IN OUT t_jobs_aat) AS
v_state user_scheduler_jobs.state%TYPE;
v_index PLS_INTEGER;
v_done BOOLEAN := TRUE;
-- Exceptions
e_job_failure EXCEPTION;
BEGIN
WHILE v_done
LOOP
v_done := FALSE;
FOR i IN p_jobs_aat.first..p_jobs_aat.last
LOOP
SELECT state INTO v_state FROM user_scheduler_jobs WHERE job_name = p_jobs_aat(i);
--dbms_output.put_line('Status: ' || v_state);
CASE
WHEN v_state = 'SUCCEEDED' OR v_state = 'COMPLETED' THEN
dbms_output.put_line(p_jobs_aat(i) || ' SUCCEEDED');
dbms_scheduler.drop_job(job_name => p_jobs_aat(i), force => TRUE);
p_jobs_aat.delete(i);
WHEN v_state = 'FAILED' OR v_state = 'BROKEN' THEN
--Exception auslösen
dbms_output.put_line(p_jobs_aat(i) || ' FAILED');
RAISE e_job_failure;
WHEN v_state = 'RUNNING' OR v_state = 'RETRY SCHEDULED' THEN
NULL;
dbms_output.put_line(p_jobs_aat(i) || ' RUNNING or RETRY SCHEDULED');
v_done := TRUE;
/*DISABLED, SCHEDULED, REMOTE, CHAIN_STALLED*/
ELSE
dbms_output.put_line(p_jobs_aat(i) || ' ELSE');
dbms_scheduler.drop_job(job_name => p_jobs_aat(i), force => TRUE);
p_jobs_aat.delete(i);
END CASE;
END LOOP;
hifa.gen_sleep(30);
END LOOP;
IF p_jobs_aat.count > 0 THEN delete_jobs_in_queue(p_jobs_aat); END IF;
EXCEPTION WHEN e_job_failure THEN
delete_jobs_in_queue(p_jobs_aat);
RAISE_APPLICATION_ERROR(-20500, 'some error message');
END check_queue_completion;
It does the trick but it seems like some awful hack.
Isn't there a better way to:
Wait until all jobs have finished.
Just run four jobs at a time and start a new one as soon as one of the running jobs has finished.
Throw an exception if one job fails or is broken.
DECLARE
cnt NUMBER:=1;
BEGIN
WHILE cnt>=1
LOOP
SELECT count(1) INTO cnt FROM dba_scheduler_running_jobs srj
WHERE srj.job_name IN ('TEST_JOB1','TEST_JOB2');
IF cnt>0 THEN
dbms_lock.sleep (5);
END IF;
END LOOP;
dbms_output.put_line('ASASA');
END;
Use dbms_alert or dbms_pipe to send/receive information about job start/finish. Query the jobs table only if you do not receive the information in expected time.
Oracle Scheduler uses Oracle Rersource Manager heavily, Just submit your jobs, defined with an end notification and have a task waiting for your event Q that counts the jobs that are submitted and the jobs that are finished.
You use Oracle Resource manager to control the maximum number of jobs to run concurrently. This will also be based on the total database load, protecting other users agains a system flooded by jobs.
In a legacy system there is some PL/SQL procedure that calls the another procedure mutliple times with different parameters. The procedure contains a lot of PL/SQL logic (if, then, else).
As the execution of this procedure takes very long, we thought about using concurrency to speed things up without even touching the actual logic.
I understand that there are several ways of running (PL/)SQL in parallel on oracle (see bellow).
However, I wasn't able to find a way to pass different arguments/parameters to a PL/SQL procedure, execute them in parallel and wait until all procedures are finished executing (i.e. I'm looking for mechanism to join all threads or for a barrier mechanism in oracle).
Let's use the following simplified example on the SCOTT Schema:
DECLARE
PROCEDURE DELETE_BONUS(
in_job IN VARCHAR2)
IS
BEGIN
-- Imagine a lot of IF, ELSEIF, ELSE statements here
DELETE FROM BONUS WHERE JOB=in_job;
END;
BEGIN
INSERT into BONUS(ENAME, JOB) SELECT ROWNUM, 'A' FROM DUAL CONNECT BY LEVEL <= 1000000;
INSERT into BONUS(ENAME, JOB) SELECT ROWNUM, 'B' FROM DUAL CONNECT BY LEVEL <= 1000000;
INSERT into BONUS(ENAME, JOB) SELECT ROWNUM, 'C' FROM DUAL CONNECT BY LEVEL <= 1000000;
-- TODO execute those in parallel
DELETE_BONUS('A');
DELETE_BONUS('B');
DELETE_BONUS('C');
-- TODO wait for all procedures to finish
EXCEPTION
WHEN OTHERS THEN
RAISE;
END;
/
Here's what I found so far:
DBMS_JOB (deprecated)
DBMS_SCHEDULER (how to wait for jobs to finish? LOCKS?)
DBMS_SCHEDULER CHAINS (passing parameters/arguments is not really possible?!)
DBMS_PARALLEL_EXECUTE (can be used to run SQL queries in parallel but not PL/SQL procedures)
Can one of these approaches be used to fork and join the procedure calls? Or is there yet another approach that can?
I solved the problem using DBMS_SCHEDULER and PIPEs for synchronization/IPC that does not rely on polling and does not need additional tables. It still wakes once per finished job, though.
It's quite some effort, so if some can propose a simpler solution please share it!
Define a procedure that calls the actual procedure that can be run
from a program/job and handles IPC (write message to pipe when finished).
Define a program that calls this procedure and defines arguments to be passed to the procedure
Define a procedure that creates a job from the program, maps parameters to job arguments and runs the job
Define logic that waits for all jobs to finish: Wait until every job has sent a message on the pipe.
--
-- Define stored procedures to be executed by job
--
/** Actual method that should be run in parallel*/
CREATE OR REPLACE PROCEDURE PROC_DELETE_TEST_BONUS(
in_job IN VARCHAR2)
IS
BEGIN
-- Imagine a lot of IF, ELSEIF, ELSE statements here
DELETE FROM TEST_BONUS WHERE JOB=in_job;
END;
/
/** Stored procedure to be run from the job: Uses pipes for job synchronization, executes PROC_DELETE_TEST_BONUS. */
CREATE OR REPLACE PROCEDURE PROC_DELETE_TEST_BONUS_CONCUR(in_pipe_name IN VARCHAR2,
in_job IN VARCHAR2)
IS
flag INTEGER;
BEGIN
-- Execute actual procedure
PROC_DELETE_TEST_BONUS(in_job);
-- Signal completion
-- Use the procedure to put a message in the local buffer.
DBMS_PIPE.PACK_MESSAGE(SYSDATE ||': Success ' ||in_job);
-- Send message, success is a zero return value.
flag := DBMS_PIPE.SEND_MESSAGE(in_pipe_name);
EXCEPTION
WHEN OTHERS THEN
-- Signal completion
-- Use the procedure to put a message in the local buffer.
DBMS_PIPE.PACK_MESSAGE(SYSDATE ||':Failed ' || in_job);
-- Send message, success is a zero return value.
flag := DBMS_PIPE.SEND_MESSAGE(in_pipe_name);
RAISE;
END;
/
--
-- Run Jobs
--
DECLARE
timestart NUMBER;
duration_insert NUMBER;
jobs_amount NUMBER := 0;
retval INTEGER;
message VARCHAR2(4000);
rows_amount NUMBER;
/** Create and define a program that calls PROG_DELETE_TEST_BONUS_CONCUR to be run as job. */
PROCEDURE create_prog_delete_test_bonus
IS
BEGIN
-- define new in each run in order to ease development. TODO Once it works, no need to redefine for each run!
dbms_scheduler.drop_program(program_name => 'PROG_DELETE_TEST_BONUS_CONCUR', force=> TRUE);
dbms_scheduler.create_program ( program_name => 'PROG_DELETE_TEST_BONUS_CONCUR', program_action =>
'PROC_DELETE_TEST_BONUS_CONCUR', program_type => 'STORED_PROCEDURE', number_of_arguments => 2,
enabled => FALSE );
dbms_scheduler.DEFINE_PROGRAM_ARGUMENT( program_name => 'PROG_DELETE_TEST_BONUS_CONCUR',
argument_position => 1, argument_name => 'in_pipe_name', argument_type => 'VARCHAR2');
dbms_scheduler.DEFINE_PROGRAM_ARGUMENT( program_name=>'PROG_DELETE_TEST_BONUS_CONCUR',
argument_position => 2, argument_name => 'in_job', argument_type => 'VARCHAR2');
dbms_scheduler.enable('PROG_DELETE_TEST_BONUS_CONCUR');
END;
/** "Forks" a job that runs PROG_DELETE_TEST_BONUS_CONCUR */
PROCEDURE RUN_TEST_BONUS_JOB(
in_pipe_name IN VARCHAR2,
in_job IN VARCHAR2,
io_job_amount IN OUT NUMBER)
IS
jobname VARCHAR2(100);
BEGIN
jobname:=DBMS_SCHEDULER.GENERATE_JOB_NAME;
dbms_scheduler.create_job(job_name => jobname, program_name =>
'PROG_DELETE_TEST_BONUS_CONCUR');
dbms_scheduler.set_job_argument_value(job_name => jobname, argument_name =>
'in_pipe_name' , argument_value => in_pipe_name);
dbms_scheduler.set_job_argument_value(job_name => jobname, argument_name =>
'in_job' , argument_value => in_job);
dbms_output.put_line(SYSDATE || ': Running job: '|| jobname);
dbms_scheduler.RUN_JOB(jobname, false );
io_job_amount:= io_job_amount+1;
END;
-- Anonymous "Main" block
BEGIN
create_prog_delete_test_bonus;
-- Define private pipe
retval := DBMS_PIPE.CREATE_PIPE(DBMS_PIPE.UNIQUE_SESSION_NAME, 100, FALSE);
dbms_output.put_line(SYSDATE || ': Created pipe: ''' || DBMS_PIPE.UNIQUE_SESSION_NAME || ''' returned ' ||retval);
timestart := dbms_utility.get_time();
INSERT into TEST_BONUS(ENAME, JOB) SELECT ROWNUM, 'A' FROM DUAL CONNECT BY LEVEL <= 1000000;
INSERT into TEST_BONUS(ENAME, JOB) SELECT ROWNUM, 'B' FROM DUAL CONNECT BY LEVEL <= 1000000;
INSERT into TEST_BONUS(ENAME, JOB) SELECT ROWNUM, 'C' FROM DUAL CONNECT BY LEVEL <= 1000000;
COMMIT;
duration_insert := dbms_utility.get_time() - timestart;
dbms_output.put_line(SYSDATE || ': Duration (1/100s): INSERT=' || duration_insert);
SELECT COUNT(*) INTO rows_amount FROM TEST_BONUS;
dbms_output.put_line(SYSDATE || ': COUNT(*) FROM TEST_BONUS: ' || rows_amount);
timestart := dbms_utility.get_time();
-- -- Process sequentially
-- PROC_DELETE_TEST_BONUS('A');
-- PROC_DELETE_TEST_BONUS('B');
-- PROC_DELETE_TEST_BONUS('C');
-- start concurrent processing
RUN_TEST_BONUS_JOB(DBMS_PIPE.UNIQUE_SESSION_NAME, 'A', jobs_amount);
RUN_TEST_BONUS_JOB(DBMS_PIPE.UNIQUE_SESSION_NAME, 'B', jobs_amount);
RUN_TEST_BONUS_JOB(DBMS_PIPE.UNIQUE_SESSION_NAME, 'C', jobs_amount);
-- "Barrier": Wait for all jobs to finish
for i in 1 .. jobs_amount loop
-- Reset the local buffer.
DBMS_PIPE.RESET_BUFFER;
-- Wait and receive message. Timeout after an hour.
retval := SYS.DBMS_PIPE.RECEIVE_MESSAGE(SYS.DBMS_PIPE.UNIQUE_SESSION_NAME, 3600);
-- Handle errors: timeout, etc.
IF retval != 0 THEN
raise_application_error(-20000, 'Error: '||to_char(retval)||' receiving on pipe. See Job Log in table user_scheduler_job_run_details');
END IF;
-- Read message from local buffer.
DBMS_PIPE.UNPACK_MESSAGE(message);
dbms_output.put_line(SYSDATE || ': Received message on '''|| DBMS_PIPE.UNIQUE_SESSION_NAME ||''' (Status='|| retval ||'): ' || message);
end loop;
dbms_output.put(SYSDATE || ': Duration (1/100s): DELETE=');
dbms_output.put_line(dbms_utility.get_time() - timestart);
SELECT COUNT(*) INTO rows_amount FROM TEST_BONUS;
dbms_output.put_line(SYSDATE || ': COUNT(*) FROM TEST_BONUS: ' || rows_amount);
retval :=DBMS_PIPE.REMOVE_PIPE(DBMS_PIPE.UNIQUE_SESSION_NAME);
dbms_output.put_line(systimestamp || ': REMOVE_PIPE: ''' || DBMS_PIPE.UNIQUE_SESSION_NAME || ''' returned: ' ||retval);
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line(SYSDATE || SUBSTR(SQLERRM, 1, 1000) || ' ' ||
SUBSTR(DBMS_UTILITY.FORMAT_ERROR_BACKTRACE, 1, 1000));
retval := DBMS_PIPE.REMOVE_PIPE(DBMS_PIPE.UNIQUE_SESSION_NAME);
dbms_output.put_line(SYSDATE || ': REMOVE_PIPE: ''' || DBMS_PIPE.UNIQUE_SESSION_NAME || ''' returned: ' ||retval);
-- Clean up in case of error
PROC_DELETE_TEST_BONUS('A');
PROC_DELETE_TEST_BONUS('B');
PROC_DELETE_TEST_BONUS('C');
RAISE;
END;
/
You should always keep in mind that the changes executed within the job are committed in a separate transaction.
Just to get a feeling for what this concurrency achieves, here a some averaged measured values: The sequential code in the question takes about 60s to complete, the parallel one about 40s.
It would be an interesting further investigation how this turns out when there are more than the three jobs running in parallel.
PS
A helpful query to find out about the status of the jobs is the following
SELECT job_name,
destination,
TO_CHAR(actual_start_date) AS actual_start_date,
run_duration,
TO_CHAR((ACTUAL_START_DATE+run_duration)) AS actual_end_date,
status,
error#,
ADDITIONAL_INFO
FROM user_scheduler_job_run_details
ORDER BY actual_start_date desc;
Just wanted to add a few notes about DBMS_PARALLEL_EXECUTE package from Oracle.
This can be used to do more than update a table, although many of the examples show this simple use case.
The trick is to use an anonymous block instead of a DML statement, and the rest of the examples are still relevant. So, instead of this:
l_sql_stmt := 'update EMPLOYEES e
SET e.salary = e.salary + 10
WHERE manager_id between :start_id and :end_id';
We might have this:
l_sql_stmt := 'BEGIN my_package.some_procedure(:start_id, :end_id); END;';
The rest of the example can be found in the "Chunk by User-Provided SQL" example section
You will still need to tell Oracle the start/end ids for each process(using CREATE_CHUNKS_BY_SQL), I typically store them in a separate lookup table (if pre-defined) or you can provide a SQL query that returns a set of start/end values. For the latter approach, try using NTILE. For example, using 8 chunks:
select min(id) as start_id, max(id) as end_id
from (
select id, ntile(8) over (order by 1) bucket
from some_table
where some_clause...
)
group by bucket
order by bucket;
Hope that helps
If yes, you can try this:
create a new table with tasks (and parameter of tasks)
create procedure that will read parameters from table, pass them to "legacy procedure" and then update task table after processing (using autonomous transactions) to show that processing is ended
outer procedure, that creates tasks, can scan task table to get information about progress. You can use DBMS_LOCK.SLEEP to wait.
DBMS_DATAPUMP doesn't fail when the columns in the source and destination tables do not match. This means that no exceptions are raised. I'm trying to use the GET_STATUS procedure in order to understand if there are any errors but unfortunately there doesn't seem to be...
My ultimate goal is for DBMS_DATAPUMP to raise an exception if the import fails. Differing columns is an easy example to work with as I know that it should fail.
Here's my current code (I've obscured schema names purposefully). The environment I'm using is identical on both servers save that I've added an extra column to the source table. I also perform a count of the number of rows in the table.
connect schema/*#db1/db1
-- */
create table tmp_test_datapump as
select u.*, cast(null as number) as break_it
from user_tables u;
Table created.
select count(*) from tmp_test_datapump;
COUNT(*)
----------
1170
connect schema/*#db2/db2
-- */
set serveroutput on
create table tmp_test_datapump as
select u.*
from user_tables u;
Table created.
In attempting to test this the DATAPUMP code has got a little more complicated. Everything in the infinite loop can be removed and this would act the same.
declare
l_handle number;
l_status varchar2(255);
l_job_state varchar2(4000);
l_ku$status ku$_status1020;
begin
l_handle := dbms_datapump.open( operation => 'IMPORT'
, job_mode => 'TABLE'
, remote_link => 'SCHEMA.DB.DOMAIN.COM'
, job_name => 'JOB_TEST_DP'
, version => 'COMPATIBLE' );
dbms_datapump.set_parameter( handle => l_handle
, name => 'TABLE_EXISTS_ACTION'
, value => 'TRUNCATE');
dbms_datapump.metadata_filter( handle => l_handle
, name => 'NAME_EXPR'
, value => 'IN (''TMP_TEST_DATAPUMP'')');
dbms_datapump.start_job(handle => l_handle);
while true loop
dbms_datapump.wait_for_job(handle => l_handle,job_state => l_status);
if l_status in ('COMPLETED','STOPPED') then
exit;
end if;
dbms_datapump.get_status( handle => l_handle
, mask => dbms_datapump.KU$_STATUS_JOB_ERROR
, job_state => l_job_state
, status => l_ku$status);
dbms_output.put_line('state: ' || l_job_state);
if l_ku$status.error is not null and l_ku$status.error.count > 0 then
for i in l_ku$status.error.first .. l_ku$status.error.last loop
dbms_output.put_line(l_ku$status.error(i).logtext);
end loop;
end if;
end loop;
end;
/
PL/SQL procedure successfully completed.
select count(*) from tmp_test_datapump;
COUNT(*)
----------
47
As you can see the number of records in the tables is different; the import has failed and no exception has been raised. Various blogs and DBA.SE questions imply that some sort of error catching can be done; but I can't seem to manage it.
How can you catch fatal errors in a DBMS_DATAPUMP import?
I'm working with dbms_datapump package right know. The following procedure is searching one table for schemas that will be exported. BACKUP_INFO_MOD is a procedure with PRAGMA AUTONOMOUS TRANSACTION that's making logs in another table.
Example 6.3 from this document helped me a lot. Here's fragment from my code (with additional commentary):
CREATE OR REPLACE PROCEDURE BACKUP_EXECUTE (
threads in number := 1
, dir in varchar2 := 'DATA_PUMP_DIR'
) AS
schemas varchar2(255);
filename varchar2(255);
path varchar2(255);
errormsg varchar2(4000);
handle number;
job_state varchar2(30);
--variables under this line are important to error handling
logs ku$_LogEntry;
lindx pls_integer;
status ku$_Status;
exporterr exception; --our exception to handle export errors
[...]
BEGIN
[...]
schemas:=schema_list(indx).schema_name;
--Full dir path for logs
select directory_path into path from dba_directories where directory_name=dir;
--If data not found then automatically raise NO_DATA_FOUND
select to_char(sysdate, 'YYMMDDHH24MI-')||lower(schemas)||'.dmp' into filename from dual;
backup_info_mod('insert',path||filename,schemas);
begin --For inner exception handling on short fragment
handle := dbms_datapump.open('EXPORT','SCHEMA');
dbms_datapump.add_file(handle, filename, dir); --dump file
dbms_datapump.add_file(handle, filename||'.log', dir,null,DBMS_DATAPUMP.KU$_FILE_TYPE_LOG_FILE); --export log file
dbms_datapump.metadata_filter(handle, 'SCHEMA_EXPR', 'IN ('''||schemas||''')');
dbms_datapump.set_parallel(handle,threads);
backup_info_mod(file_name=>path||filename, curr_status=>'IN PROGRESS');
dbms_datapump.start_job(handle);
--If job didn't start due to some errors, then let's get some information
exception
when others then
dbms_datapump.get_status(handle,8,0,job_state,status);
--This will overwrite our job_state and status
end;
--Let's go handle error if job_state was overwritten
if job_state is not null then
raise exporterr;
else
job_state:='UNDEFINED';
end if;
--Checking in loop if errors occurred. I'm not using wait_for_job
--because it didn't work out
while (job_state != 'COMPLETED') and (job_state != 'STOPPED') loop
--Like before, let's get some information
dbms_datapump.get_status(handle,8,-1,job_state,status);
--Looking for errors using mask
if (bitand(status.mask,dbms_datapump.ku$_status_job_error) != 0) then
--If occurred: let's stop the export job and raise an error
dbms_datapump.stop_job(handle);
dbms_datapump.detach(handle);
raise exporterr;
exit;
end if;
end loop;
backup_info_mod(file_name=>path||filename, curr_status=>'COMPLETED');
dbms_datapump.detach(handle);
exception
when NO_DATA_FOUND then
backup_info_mod('insert',null,schemas,'ERROR','No '||dir||' defined in dba_directories');
when exporterr then
--Let's get all error messages and write it to errormsg variable
logs:=status.error;
lindx:=logs.FIRST;
while lindx is not null loop
errormsg:=errormsg||logs(lindx).LogText;
lindx:=logs.NEXT(lindx);
if lindx is not null then
errormsg:=errormsg||' | '; --Just to separate error messages
end if;
end loop;
backup_info_mod(
file_name=>path||filename,
curr_status=>'ERROR',
errormsg=>errormsg);
/*when other then --TODO
null;
*/
end;
END BACKUP_EXECUTE;
You can put the datapump command in a shell script when the log is created for the impdp, and before you end the shell script you can check the log for IMP- errors or ORA- errors, if true warn the user to look at the log file for errors.
The provided document from yammy is good.
I faced the same problem when using the dbms_datapump package to import a DB dump. Even there are error during the import, the job is treated as success / finished. By using the code example in the document, i could get the error / log message during the import. I then check if there are 'ORA-' found in the log message and throw a custom error when the import job is finished.
Following is the sample code:
PROMPT CREATE OR REPLACE PROCEDURE import_schema
CREATE OR REPLACE PROCEDURE import_db_dump (
dumpFilename IN VARCHAR2)
IS
handle NUMBER; -- Handler of the job
loopIdx NUMBER; -- Loop index
percentDone NUMBER; -- Percentage of job complete
jobState VARCHAR2(30); -- To keep track of job state
ku_logEntry ku$_LogEntry; -- For WIP and error messages
ku_jobStatus ku$_JobStatus; -- The job status from get_status
ku_jobDescjd ku$_JobDesc; -- The job description from get_status
ku_Status ku$_Status; -- The status object returned by get_status
errorCount NUMBER;
import_error_found EXCEPTION;
BEGIN
handle := dbms_datapump.open (
operation => 'IMPORT',
job_mode => 'SCHEMA');
dbms_output.put_line('Define table exists action: truncate');
dbms_datapump.set_parameter (
handle => handle,
name => 'TABLE_EXISTS_ACTION',
value => 'TRUNCATE');
dbms_output.put_line('Define dumpfilename: ' || dumpFilename);
dbms_datapump.add_file (
handle => handle,
filename => dumpFilename,
filetype => dbms_datapump.ku$_file_type_dump_file);
dbms_output.put_line('Start datapump job');
dbms_output.put_line('==================');
dbms_datapump.start_job (handle);
-- Ref: http://docs.oracle.com/cd/E11882_01/server.112/e22490/dp_api.htm#SUTIL977
-- The import job should now be running. In the following loop, the job is
-- monitored until it completes. In the meantime, progress information is
-- displayed.
percentDone := 0;
jobState := 'UNDEFINED';
errorCount := 0;
WHILE (jobState != 'COMPLETED') AND (jobState != 'STOPPED') LOOP
dbms_datapump.get_status(handle,
dbms_datapump.ku$_status_job_error +
dbms_datapump.ku$_status_job_status +
dbms_datapump.ku$_status_wip,
-1, jobState, ku_Status);
ku_jobStatus := ku_Status.job_status;
-- If the percentage done changed, display the new value.
IF ku_jobStatus.percent_done != percentDone THEN
dbms_output.put_line('*** Job percent done = ' ||
to_char(ku_jobStatus.percent_done));
percentDone := ku_jobStatus.percent_done;
END IF;
-- If any work-in-progress (WIP) or Error messages were received for the job,
-- display them.
IF (bitand(ku_Status.mask, dbms_datapump.ku$_status_wip) != 0) THEN
ku_logEntry := ku_Status.wip;
ELSE
IF (bitand(ku_Status.mask,dbms_datapump.ku$_status_job_error) != 0) THEN
ku_logEntry := ku_Status.error;
ELSE
ku_logEntry := null;
END IF;
END IF;
IF ku_logEntry IS NOT NULL THEN
loopIdx := ku_logEntry.FIRST;
WHILE loopIdx IS NOT NULL LOOP
dbms_output.put_line(ku_logEntry(loopIdx).LogText);
IF INSTR(ku_logEntry(loopIdx).LogText, 'ORA-') > 0 THEN
errorCount := errorCount + 1;
dbms_output.put_line('^^^^---ERROR FOUND');
END IF;
loopIdx := ku_logEntry.NEXT(loopIdx);
END LOOP;
END IF;
END LOOP;
-- Indicate that the job finished and gracefully detach from it.
dbms_output.put_line('Job has completed');
dbms_output.put_line('Final job state = ' || jobState);
dbms_datapump.detach(handle);
IF errorCount > 0 THEN
RAISE import_error_found;
END IF;
EXCEPTION
WHEN import_error_found THEN
dbms_output.put_line('Error found when import. Number of error: ' || errorCount);
RAISE;
WHEN OTHERS THEN
dbms_output.put_line('[Error Backtrace]');
dbms_output.put_line(dbms_utility.format_error_backtrace());
dbms_output.put_line('[Call Stack]');
dbms_output.put_line(dbms_utility.format_call_stack());
dbms_datapump.stop_job(handle);
RAISE;
END;
/
Hope this help.