Wednesday, January 29, 2020

Oracle Unique Index Non-Consistency Read with RowCR

RowCR is an optimization to avoid constructing a CR block if the unique index accessed row is not in an uncommitted transaction. If a row is updated and committed after a query started using unique index, instead of reading undo block and cloning a before image, Oracle fetches the row from modified block (after image). It results in a wrong result since it is no more a consistent read.

Blog: Oracle Consistency Read Changed made an extensive test of RowCR behaviors and contains a short description on RowCR:
     A brief overview of this optimization is that we try to avoid rollbacks while constructing a CR block 
     if the present block has no uncommitted changes.
and cited some Oracler's confirmation: "We have RowCR Optimization turned on by default, which may not be appropriate."

In this Blog, we will repeat the same tests as above Blog with:
  -.Sql Trace Event (10046)
  -.Consistent Read Event (10200)
  -.Dtrace 
so that we can try to have a further understanding of RowCR internals.
(see Blog: Dynamic tracing of Oracle logical I/O: part 2. Dtrace LIO v2 is released
and right linked book: Oracle Database Performance Tuning (Studies . Practices . Researches) - Chapter 1: Data Accesses),

Parallel to the discussion of Read Consistency, we will give a look of Write Consistency achieved by DML Restart, and show a new way to track exact three update starts.

Note:
     -. All tests done in 12c, 18c, 19c.
     -. Trace files are shortened by removing irrelevant text, and commented by meta data names (started with "<==").
     -. As of Oracle 11gR2, "_row_cr" is TRUE in default. The feature can be disabled by setting "_row_cr" = FALSE


1. Test Setup


We first create a table and two indexes, one unique, another non-unique. Then create two Plsql procedures, one is a query procedure with two indexes, and another is an update procedure. In the query procedure, open a cursor and then go to sleep for a few seconds so that during the sleep, update procedure is executed.

---====================== Test Setup ======================---

drop table rowcr_tab purge;

create table rowcr_tab (id_uniq, id_non_uniq, color, ts, seq) 
   as select level, level, 'BLACK', localtimestamp, 0 from dual connect by level <= 1e3;

create unique index rowcr_tab#id_uniq on rowcr_tab(id_uniq);

--alter index rowcr_tab#id_uniq rebuild reverse;
--alter index rowcr_tab#id_uniq rebuild noreverse;

create        index rowcr_tab#id_non_uniq on rowcr_tab(id_non_uniq);

exec dbms_stats.gather_table_stats(null, 'ROWCR_TAB', cascade=>true);

create or replace procedure rowcr_read_u1n2 (p_test varchar2, p_id number, p_sleep number) as
  cursor c_uniq is select /*+ index(t rowcr_tab#id_uniq) */ * from rowcr_tab t where id_uniq = p_id;
  cursor c_non_uniq is select /*+ index(t rowcr_tab#id_non_uniq) */ * from rowcr_tab t where id_non_uniq = p_id;
  --cursor c_rowid is select * from rowcr_tab t where rowid = chartorowid('AAJXn8AAAAAANCVADN');
  l_row          rowcr_tab%rowtype;
begin
  if p_test in ('all', 'uniq') then open c_uniq; end if;
  if p_test in ('all', 'non_uniq') then open c_non_uniq; end if;
  dbms_output.put_line('-------------------- Cursor Opened at '|| localtimestamp);
  
  dbms_lock.sleep(p_sleep);
  
  if p_test in ('all', 'uniq') then 
   fetch c_uniq into l_row;
   while (c_uniq%found) loop
     dbms_output.put_line('Unqiue     Index Read Color: '||l_row.color||' at '|| l_row.ts);
     fetch c_uniq into l_row;
   end loop;
   close c_uniq;
  end if;
    
  if p_test in ('all', 'non_uniq') then 
   fetch c_non_uniq into l_row;
   while (c_non_uniq%found) loop
     dbms_output.put_line('Non-Unqiue Index Read Color: '||l_row.color||' at '|| l_row.ts);
     fetch c_non_uniq into l_row;
   end loop;
   close c_non_uniq;
 end if;
end;
/

create or replace procedure rowcr_update (p_id number, p_commit boolean) as
  l_old_val varchar2(50);
  l_new_val varchar2(50);
begin
  select 'Old Color: '||color||' at '||ts into l_old_val from rowcr_tab t where id_uniq = p_id;
  dbms_output.put_line(l_old_val);
  dbms_output.put_line('--------- Update at '|| localtimestamp);
  update rowcr_tab set color=decode(color, 'BLACK', 'WHITE', 'WHITE', 'BLACK'), ts=localtimestamp where id_uniq=p_id;
  if p_commit then 
     commit;
  end if;
  select 'New Color: '||color||' at '||ts into l_new_val from rowcr_tab t where id_uniq = p_id;
  dbms_output.put_line(l_new_val);
end;
/


2. Collect Meta Data


Then we list the Meta Data which are referenced in later trace files.

---====================== Collect Meta Data ======================---

select object_name, object_id, object_type, to_char(object_id, 'XXXXXXXXXXX') objd_hex 
  from dba_objects where object_name like 'ROWCR_TAB%';
  
OBJECT_NAME            OBJECT_ID  OBJECT_TYPE  OBJD_HEX
---------------------  ---------  -----------  --------
ROWCR_TAB              2456060    TABLE        2579FC
ROWCR_TAB#ID_UNIQ      2456061    INDEX        2579FD
ROWCR_TAB#ID_NON_UNIQ  2456062    INDEX        2579FE

select segment_name, segment_type, tablespace_name, header_file, header_block,
       to_char(header_file, 'xxxxxx') header_file_hex, to_char(header_block+1, 'xxxxxxx') header_block_hex
  from dba_segments where segment_name like 'ROWCR_TAB%';

SEGMENT_NAME           SEGMENT_TYPE  TABLESPACE_NAME  HEADER_FILE  HEADER_BLOCK  HEADER_FILE_HEX  HEADER_BLOCK_HEX
---------------------  ------------  ---------------  -----------  ------------  ---------------  ----------------
ROWCR_TAB              TABLE         U1               1548         53394             60c              d093
ROWCR_TAB#ID_UNIQ      INDEX         U1               1548         53402             60c              d09b
ROWCR_TAB#ID_NON_UNIQ  INDEX         U1               1548         53410             60c              d0a3

select name, ts#, to_char(ts#, 'xxxxx') ts#_hex from v$tablespace where name='U1';

NAME  TS#   TS#_HEX
----  ----  -------
U1    1999     7cf

select indx, kcbwhdes from sys.x_kcbwh where indx in (1298, 1299, 1004, 1007, 61, 1061);

INDX  KCBWHDES
----  ----------------
61    ktuwh27: kturbk
1004  kdswh02: kdsgrp
1007  kdswh05: kdsgrp
1061  kdiwh16: kdifxs
1298  qeilwhrp: qeilbk
1299  qeilwhnp: qeilbk

select rowid rd, t.* from rowcr_tab t where id_uniq=678 or id_non_uniq=678;

RD                    ID_UNIQ ID_NON_UNIQ COLOR TS
------------------ ---------- ----------- ----- ---------------------------
AAJXn8AAAAAANCVADN        678         678 BLACK 23.01.20 15:21:21.903262000


3. Run Test


We open two Sqlplus sessions. In the first session, we open cursor, sleep 30 seconds, then fetch row from opened cursor. In the second session, we make update on that same row within above 30 sleeping seconds. Here the test output from both sessions:

---========== Session-1 Run @T1, Wait 30 Seconds ==========---

15:05:41 TESTDB(111)> exec rowcr_read_u1n2(p_test => 'all', p_id => 678, p_sleep => 30);

  -------------------- Cursor Opened at 23-JAN-2020 15:05:56
  Unqiue     Index Read Color: WHITE at 23-JAN-2020 15:06:09
  Non-Unqiue Index Read Color: BLACK at 23-JAN-2020 12:42:49


---========== Session-2 Run @T2 (T2 < T1 + 30) ==========---

15:06:05 TESTDB(222)> exec rowcr_update(678, true);

  Old Color: BLACK at 23-JAN-2020 12:42:49
  -------------------- Update at 23-JAN-2020 15:06:09
  New Color: WHITE at 23-JAN-2020 15:06:09
In Session-2, the color in row (id = 678) is updated from BLACK to WHITE and committed. In Session-1, Unqiue Index Read returns WHITE, which is the after image even though the cursor was opened before update, whereas Non-Unqiue Index Read returns BLACK, which is the before image.

The consequence is that Unqiue Index Read returns a wrong Non-Consistency result, whereas Non-Unqiue Index Read gives the correct Consistency result.

Now we run two tests with Sql Trace Event (10046), Consistent Read Event (10200) and Dtrace.


3.1. Unique Index Read


Here the test and output:

---========== Session-1 Run @T1, Wait 30 Seconds ==========---

alter session set events='10046 trace name context forever, level 12: 
                          10200 trace name context forever, level 10' 
      tracefile_identifier='10046_10200_rowcr_uniq_1';
      
exec rowcr_read_u1n2(p_test => 'uniq', p_id => 678, p_sleep => 30);

alter session set events='10046 trace name context off : 10200 trace name context off ';


15:08:24 TESTDB(111)> exec rowcr_read_u1n2(p_test => 'uniq', p_id => 678, p_sleep => 30);
  
  -------------------- Cursor Opened at 23-JAN-2020 15:08:24
  Unqiue     Index Read Color: BLACK at 23-JAN-2020 15:08:40


---========== Session-2 Run @T2 (T2 < T1 + 30) ==========---

15:08:38 TESTDB(222)> exec rowcr_update(p_id => 678, p_commit => true);

  Old Color: WHITE at 23-JAN-2020 15:06:09
  -------------------- Update at 23-JAN-2020 15:08:40
  New Color: BLACK at 23-JAN-2020 15:08:40


3.1.1. Unique Index - Sql Trace



SELECT /*+ index(t rowcr_tab#id_uniq) */ * FROM ROWCR_TAB T WHERE ID_UNIQ = :B1 

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        0      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.00       0.00          0          3          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3      0.00       0.00          0          3          0           1

Row Source Operation
---------------------------------------------------
TABLE ACCESS BY INDEX ROWID ROWCR_TAB (cr=3 pr=0 pw=0 time=593 us cost=1 size=25 card=1)
 INDEX UNIQUE SCAN ROWCR_TAB#ID_UNIQ (cr=2 pr=0 pw=0 time=187 us cost=1 size=0 card=1)(object id 2456061)


3.1.2. Unique Index - Consistent Read Trace (Event 10200)



ktrgtc2(): started for block <0x07cf : 0x0000d09b> objd: 0x002579fd    <== ROWCR_TAB#ID_UNIQ root
ktrexc(): returning 2 on:  ffff80ffbccf4e70  
ktrgtc2(): completed for block <0x07cf : 0x0000d09b> objd: 0x002579fd

ktrgtc2(): started for block <0x07cf : 0x0000d09d> objd: 0x002579fd    <== ROWCR_TAB#ID_UNIQ leaf
ktrexc(): returning 2 on:  ffff80ffbccf4e70  
ktrgtc2(): completed for block <0x07cf : 0x0000d09d> objd: 0x002579fd

ktrgtc2(): started for block <0x07cf : 0x0000d095> objd: 0x002579fc    <== ROWCR_TAB
ktrexc(): returning 9 on:  ffff80ffbccf4e70 
ktrgtc2(): completed for block <0x07cf : 0x0000d095> objd: 0x002579fc


3.1.3. Unqiue Index - Dtrace



$ dtracelio.d 1111

Dynamic tracing of Oracle logical I/O v2.1 by Alexander Anokhin ( http://alexanderanokhin.wordpress.com )

  kcbgtcr(0xFFFF80FFBFFF39E0,1,1298,0) [tsn: 1999 rdba: 0xd09b (0/53403) obj: 2456061] where: 1298 exam: 1  <== ROWCR_TAB#ID_UNIQ root
  kcbgtcr(0xFFFF80FFBC1446E8,1,1299,0) [tsn: 1999 rdba: 0xd09d (0/53405) obj: 2456061] where: 1299 exam: 1  <== ROWCR_TAB#ID_UNIQ leaf
  kcbgtcr(0xFFFF80FFBC14C518,1,1004,0) [tsn: 1999 rdba: 0xd095 (0/53397) obj: 2456060] where: 1004 exam: 1  <== ROWCR_TAB

===================== Logical I/O Summary (grouped by object/function) ==============
 function    stat   object_id   data_object_id   mode_held   where     bufs     calls
--------- ------- ----------- ---------------- ----------- ------- -------- ---------
  kcbgtcr      cr     2456060          2456060                1004        1         1
  kcbgtcr      cr     2456061          2456061                1298        1         1
  kcbgtcr      cr     2456061          2456061                1299        1         1
=====================================================================================

============================= Logical I/O Summary (grouped by object) =============================
 object_id  data_object_id   lio   cr   cr (e)   cr (d)   cu   cu (d) ispnd (Y) ispnd (N)   pin rls
---------- --------------- ----- ---- -------- -------- ---- -------- --------- --------- ---------
         0               0     0    0        0        0    0        0         0         1         0
   2456060         2456060     1    1        1        0    0        0         0         1         1   <== ROWCR_TAB
   2456061         2456061     2    2        2        0    0        0         0         1         0   <== ROWCR_TAB#ID_UNIQ
---------- --------------- ----- ---- -------- -------- ---- -------- --------- --------- ---------
     total                     3    3        3        0    0        0         0         3         1
===================================================================================================

Legend
  lio      : logical gets (cr + cu)
  cr       : consistent gets
  cr (e)   : consistent gets - examination
  cr (d)   : consistent gets direct
  cu       : db block gets
  cu (d)   : db block gets direct
  ispnd (Y): buffer is pinned count
  ispnd (N): buffer is not pinned count
  pin rls  : pin releases

where
  INDX  KCBWHDES
  ----  ----------------
  1298  qeilwhrp: qeilbk
  1299  qeilwhnp: qeilbk
  1004  kdswh02: kdsgrp  
The above trace files showed that Unique Index made 2 ROWCR_TAB#ID_UNIQ cr reads and 1 ROWCR_TAB cr read, without any UNDO read.

If we dump the block, we can see that the fetched row has been updated and committed (commit SCN) after query started (query start SCN), and when query read the row using unique index, it saw the row already committed, and simply returned it without any SCN checking although (query start SCN < commit SCN).


3.2. Non-Unique Index Read



---========== Session-1 Run @T1, Wait 30 Seconds ==========---

alter session set events='10046 trace name context forever, level 12: 
                          10200 trace name context forever, level 10' 
      tracefile_identifier='10046_10200_rowcr_non_uniq_1';
      
exec rowcr_read_u1n2(p_test => 'non_uniq', p_id => 678, p_sleep => 30);

alter session set events='10046 trace name context off : 10200 trace name context off ';


15:17:34 TESTDB(111)> exec rowcr_read_u1n2(p_test => 'non_uniq', p_id => 678, p_sleep => 30);

  -------------------- Cursor Opened at 23-JAN-2020 15:17:34
  Non-Unqiue Index Read Color: WHITE at 23-JAN-2020 15:12:53


---========== Session-2 Run @T2 (T2 < T1 + 30) ==========---

15:17:25 TESTDB(222)> exec rowcr_update(p_id => 678, p_commit => true);
  Old Color: WHITE at 23-JAN-2020 15:12:53
  -------------------- Update at 23-JAN-2020 15:17:43
  New Color: BLACK at 23-JAN-2020 15:17:43


3.2.1. Non-Unique Index - Sql Trace



********************************************************************************

SELECT /*+ index(t rowcr_tab#id_non_uniq) */ * FROM ROWCR_TAB T WHERE ID_NON_UNIQ = :B1 


call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        0      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.00       0.00          0          5          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3      0.00       0.00          0          5          0           1

 Row Source Operation
 ---------------------------------------------------
 TABLE ACCESS BY INDEX ROWID BATCHED ROWCR_TAB (cr=5 pr=0 pw=0 time=778 us cost=1 size=25 card=1)
  INDEX RANGE SCAN ROWCR_TAB#ID_NON_UNIQ (cr=3 pr=0 pw=0 time=422 us cost=1 size=0 card=1)(object id 2456062)


3.2.2. Non-Unique Index - Consistent Read Trace (Event 10200)



ktrgtc2(): started for block <0x07cf : 0x0000d0a3> objd: 0x002579fe      <== ROWCR_TAB#ID_NON_UNIQ root
ktrexc(): returning 2 on:  ffff80ffbccf4e70  
ktrgtc2(): completed for block <0x07cf : 0x0000d0a3> objd: 0x002579fe

ktrget2(): started for block  <0x07cf : 0x0000d0a5> objd: 0x002579fe     <== ROWCR_TAB#ID_NON_UNIQ leaf
ktrexf(): returning 9 on:  ffff80ffbccf4e70 
ktrgcm(): completed for block  <0x07cf : 0x0000d0a5> objd: 0x002579fe
ktrget2(): completed for  block <0x07cf : 0x0000d0a5> objd: 0x002579fe

ktrget2(): started for block  <0x07cf : 0x0000d095> objd: 0x002579fc     <== ROWCR_TAB
ktrexf(): returning 9 on:  ffff80ffbccf4e70 
kcbchg updating CR fields for 0xa0f720d8, 53397; 896:7b82885d            <== read UNDO block
kcbchg new CR fields for 0xa0f720d8, 53397; 896:7b82885d
ktrgcm(): completed for block  <0x07cf : 0x0000d095> objd: 0x002579fc
ktrget2(): completed for  block <0x07cf : 0x0000d095> objd: 0x002579fc

ktrget2(): started for block  <0x07cf : 0x0000d0a5> objd: 0x002579fe     <== ROWCR_TAB#ID_NON_UNIQ leaf
ktrexf(): returning 9 on:  ffff80ffbccf4e70                              <== INDEX RANGE SCAN next read
ktrgcm(): completed for block  <0x07cf : 0x0000d0a5> objd: 0x002579fe
ktrget2(): completed for  block <0x07cf : 0x0000d0a5> objd: 0x002579fe


3.2.3. Non-Unique Index - Dtrace



$ dtracelio1t.d 1111

Dynamic tracing of Oracle logical I/O v2.1 by Alexander Anokhin ( http://alexanderanokhin.wordpress.com )

  kcbgtcr(0xFFFF80FFBC147A40,1,1298,0)          [tsn: 1999 rdba: 0xd0a3   (0/53411) obj: 2456062] where: 1298 exam: 1    <== ROWCR_TAB#ID_NON_UNIQ root
  kcbgtcr(0xFFFF80FFBC147A40,0,1299,4033943420) [tsn: 1999 rdba: 0xd0a5   (0/53413) obj: 2456062] where: 1299 exam: 0    <== ROWCR_TAB#ID_NON_UNIQ leaf
  kcbgtcr(0xFFFF80FFBC1463C8,0,1007,0)          [tsn: 1999 rdba: 0xd095   (0/53397) obj: 2456060] where: 1007 exam: 0    <== ROWCR_TAB
  kcbgtcr(0xFFFF80FFBFFF1FB0,1,61,0)            [tsn:    2 rdba: 0xc051ef (3/20975) obj: 0  dobj: -1] where: 61 exam: 1  <== UNDO block
  kcbgtcr(0xFFFF80FFBC147790,0,1061,0)          [tsn: 1999 rdba: 0xd0a5   (0/53413) obj: 2456062] where: 1061 exam: 0    <== ROWCR_TAB#ID_NON_UNIQ leaf

===================== Logical I/O Summary (grouped by object/function) ==============
 function    stat   object_id   data_object_id   mode_held   where     bufs     calls
--------- ------- ----------- ---------------- ----------- ------- -------- ---------
  kcbgtcr      cr           0               -1                  61        1         1
  kcbgtcr      cr     2456060          2456060                1007        1         1
  kcbgtcr      cr     2456062          2456062                1061        1         1
  kcbgtcr      cr     2456062          2456062                1298        1         1
  kcbgtcr      cr     2456062          2456062                1299        1         1
=====================================================================================

============================= Logical I/O Summary (grouped by object) =============================
 object_id  data_object_id   lio   cr   cr (e)   cr (d)    cu  cu (d) ispnd (Y) ispnd (N)   pin rls
---------- --------------- ----- ---- -------- -------- ----- ------- --------- --------- ---------
         0               0     0    0        0        0     0       0         0         1         0
   2456060         2456060     1    1        0        0     0       0         0         1         1   <== ROWCR_TAB
         0              -1     1    1        1        0     0       0         0         0         0   <== UNDO block
   2456062         2456062     3    3        1        0     0       0         0         2         2   <== ROWCR_TAB#ID_NON_UNIQ
---------- --------------- ----- ---- -------- -------- ----- ------- --------- --------- ---------
     total                     5    5        2        0     0       0         0         4         3
===================================================================================================

where
  INDX  KCBWHDES
  ----  ----------------
  1298  qeilwhrp: qeilbk
  1299  qeilwhnp: qeilbk
  1007  kdswh05: kdsgrp
  61    ktuwh27: kturbk  
  1061  kdiwh16: kdifxs  
The above trace files showed that Non-Unique Index made 3 ROWCR_TAB#ID_UNIQ cr reads, 1 ROWCR_TAB cr read, and additionally 1 UNDO read to re-construct before image.


4. RowCR Documentation


Web page: RE: _row_cr setting in RAC has a detailed explanation about Row CR:
  In Oracle9i Release 2, the Row CR feature can partially alleviate the overhead of the global block cleanout/rollback problem. 
  The Row CR feature will reduce the number of CR rollbacks and avoid a costly block cleanout/rollback in a RAC environment. 
  Instead of performing a block cleanout, Row CR will only attempt to generate a CR version for the particular row. 
  Currently, Row CR works for UPDATE statements that have a Unique Index Scan or Fetch by Row ID in the row source. 
  Whenever there is a Fetch by Unique Index row source in the execution plan, that causes CR cleanout / rollback, 
  Row CR will kick in. The statistic Row CR attempts essentially measures the number of updates that have this property 
  that cause CR cleanout or rollbacks to happen. The statistic Row CR hits measures the success ratio of 
  Row CR among the attempts made. Oracle10g and beyond this feature should extend for index range scans, 
  as well as further optimize cleanout processing. 
  
  To extend Row CR functionality, the hidden parameter _row_cr needs to be set to TRUE (default is FALSE). 
  Row CR will be tried before invoking CR Rollback. The parameter can be changed dynamically, by using ALTER SYSTEM. 
  Currently, Row CR is turned on only for Updates whose plan is Unique Scans or Fetch by Row ID. 
  No other operation gets Row CR. When setting the _row_cr parameter to TRUE, Row CR will be attempted for ALL SQL 
  (this will include Updates, Selects, Deletes and Joins) that have a Fetch By Unique Index 
  OR a Fetch BY Row ID row source in the row source tree.
  
  This parameter has benefited several bugs, however this is unsupported functionality. Consider setting _row_cr=TRUE 
  if majority are immediate CR cleanouts
  
  Scott  
Oracle MOS Doc: ORA-00600: [KTRGCM_3] (Doc ID 424779.1) wrote:
     "_row_cr" is a hidden parameter used to control CR requests and Buffer waits on remote undo segment headers 
     which is a common problem on RAC instances
     
     RowCR is now enabled and supported with 10.2. RowCR can partially reduce the overhead of 
     the global block cleanout/rollback problem by simply attempting to generate a CR version for the requested row. 
     
     Please note that RowCR (_row_cr=TRUE) is NOT SUPPORTED in Oracle versions prior to 10g Release.
Reading above documentation, it looks like that RowCR is an optimization, originally implemented for RAC.


5. ktrexc, ktrexf and Buffer Pin


Above trace files showed two code paths of logical read:
     ktrgtc2->kcbgtcr->ktrexc: for unique index and index root
     ktrgtc2->kcbgtcr->ktrexf: for non-unique index
For unique index, the first path with ktrexc is used. For index root block, the same ktrexc path is used.

According to Blog: Buffer is pinned count:

Notice that function ktrexc is called in kcbgtcr. It is examination - the case when Oracle just read the buffer and does not pin it. The statistic "consistent gets - examination" is incremented inside this function.

It has a deep discussion with Dtrace on statistics "buffer is pinned count" and "buffer is not pinned count". It looks like that:
     index root block is never pinned.
     non-unique index leaf block is always pinned.
     table block is checked and pinned.


6. Query without using Cursor


In the above test, we observed this Non-Consistency Read with cursor.

Now we can also make one more test without using cursor, and check if there still exists such Non-Consistency Read.

At first, create a sleeping function to postpone query row fetch.

create or replace function row_sleep(p_id number, p_seconds number) return number as 
begin
  dbms_output.put_line('Sleeping seconds: '||p_seconds||' for id: '||p_id||' at '||localtimestamp);
  dbms_lock.sleep(p_seconds);
  return p_id;
end;
/
Then run following statement in Session-1, and same previous update statement in Session-2.

  select t.* from rowcr_tab t where id_uniq in (1, 678);
  select t.* from rowcr_tab t where id_uniq in (1, 678) and row_sleep(id_uniq, 10) = id_uniq;
  select t.* from rowcr_tab t where id_uniq in (1, 678);
Here the test out put from both sessions:

---========== Session-1 Run @T1, Wait 40 Seconds ==========---

09:23:02 TESTDB(111)> select t.* from rowcr_tab t where id_uniq in (1, 678);

  ID_UNIQ ID_NON_UNIQ COLOR TS
  ------- ----------- ----- --------------------
        1           1 BLACK 23-JAN-2020 11:16:45
      678         678 BLACK 24-JAN-2020 09:22:52

09:23:10 TESTDB(111)> select t.* from rowcr_tab t where id_uniq in (1, 678) and row_sleep(id_uniq, 10) = id_uniq;

  ID_UNIQ ID_NON_UNIQ COLOR TS
  ------- ----------- ----- --------------------
        1           1 BLACK 23-JAN-2020 11:16:45
      678         678 BLACK 24-JAN-2020 09:22:52
  
  Sleeping seconds: 10 for id:   1 at 24-JAN-2020 09:23:10
  Sleeping seconds: 10 for id:   1 at 24-JAN-2020 09:23:20
  Sleeping seconds: 10 for id: 678 at 24-JAN-2020 09:23:30
  Sleeping seconds: 10 for id: 678 at 24-JAN-2020 09:23:40
  
  Elapsed: 00:00:40.09

09:23:50 TESTDB(111)> select t.* from rowcr_tab t where id_uniq in (1, 678);

  ID_UNIQ ID_NON_UNIQ COLOR TS
  ------- ----------- ----- --------------------
        1           1 BLACK 23-JAN-2020 11:16:45
      678         678 WHITE 24-JAN-2020 09:23:17
    
    
---========== Session-2 Run @T2 (T2 < T1 + 20) ==========---

09:22:53 TESTDB(222)> exec rowcr_update(p_id => 678, p_commit => true);
  Old Color: BLACK at 24-JAN-2020 09:22:52
  -------------------- Update at 24-JAN-2020 09:23:17
  New Color: WHITE at 24-JAN-2020 09:23:17
The test output shows that query made Consistency Read when not using cursor. The query returned before image (BLACK for id 678) when the row with id 678 was updated to WHITE after query started.

By the way, we can see that query sleeps 40 seconds although only two rows are selected with each of 10 seconds. The two additional of 10 seconds are due to filter predicate in Rowsource Id 2 (TABLE ACCESS BY INDEX ROWID), which is visible in XPLAN.

--------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name              | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                   |     1 |    25 |     1   (0)| 00:00:01 |
|   1 |  INLIST ITERATOR             |                   |       |       |            |          |
|*  2 |   TABLE ACCESS BY INDEX ROWID| ROWCR_TAB         |     1 |    25 |     1   (0)| 00:00:01 |
|*  3 |    INDEX UNIQUE SCAN         | ROWCR_TAB#ID_UNIQ |     1 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------

  Predicate Information (identified by operation id):
  ---------------------------------------------------
     2 - filter("ROW_SLEEP"("ID_UNIQ",10)=1 OR "ROW_SLEEP"("ID_UNIQ",10)=678)
     3 - access("ID_UNIQ"=1 OR "ID_UNIQ"=678)
         filter("ID_UNIQ"="ROW_SLEEP"("ID_UNIQ",10))
However, if we select one single row, the sleeping time is 10 seconds (not 20 seconds) because there is no more filter predicate in Rowsource Id 1 (TABLE ACCESS BY INDEX ROWID). Therefore, the additional filter predicate is only generated for selection of multiple rows.
         
09:43:02 TESTDB(111)> select t.* from rowcr_tab t where id_uniq in (678) and row_sleep(id_uniq, 10) = id_uniq;

  ID_UNIQ ID_NON_UNIQ COLOR TS
  ------- ----------- ----- --------------------
      678         678 WHITE 30-JAN-2020 09:23:17
  
  1 row selected.
  Sleeping seconds: 10 for id: 678 at 30-JAN-2020 09:43:03
  Elapsed: 00:00:10.05     
  
-------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name              | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                   |     1 |    25 |     1   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| ROWCR_TAB         |     1 |    25 |     1   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | ROWCR_TAB#ID_UNIQ |     1 |       |     1   (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------  

  Predicate Information (identified by operation id):
  ---------------------------------------------------
     2 - access("ID_UNIQ"=678)
         filter("ROW_SLEEP"("ID_UNIQ",10)=678)  


7. Write Consistency and DML Restart


Following above discussion of Read Consistency, we can also have a look of Write Consistency achieved with DML Restart.

Book: Expert Oracle Database Architecture (3rd ed. Edition) Page 267-274 described Write consistency and showed update restart with before row trigger. Tanel's video: Oracle SQL Monitoring and Write Consistency Demo used V$SQL_PLAN_MONITOR and SQL trace to demonstrate update restart. Blog: Write consistency and DML restart revealed internal "updThreePhaseExe" mechanism: the implementation of multiple time update restarts and explored update restart with DML Trace.

In this section, we will show 3 update starts caused by DML restart with previous defined delay function row_sleep. In both Session-1 and Session-2, we run the same update (and commit) one immediately after another:

---========== Session-1 Run @T1 ==========---
alter session set tracefile_identifier = 'trc_1';
alter system set events 'trace[DML] {callstack: fname dmlTrace} disk=high trace("DML restarted sqlid : %\n", sqlid())';
alter session set events '10046 trace name context forever, level 12';

update /*+ GATHER_PLAN_STATISTICS MONITOR Upd_1 */ rowcr_tab 
   set id_uniq = -id_uniq, ts=localtimestamp
 where id_uniq in (-678, 678)   --id_uniq+0 avoids multi evaluation of filter
   and row_sleep(id_uniq, 10) = id_uniq+0;    
 
commit;

alter session set events '10046 trace name context off';

---========== Session-2 Run @T2 ==========---
alter session set tracefile_identifier = 'trc_2';
alter system set events 'trace[DML] {callstack: fname dmlTrace} disk=high trace("DML restarted sqlid : %\n", sqlid())';
   -- DML restarted sqlid : fsg7u662gr5kj
alter session set events '10046 trace name context forever, level 12';
 				
update /*+ GATHER_PLAN_STATISTICS MONITOR Upd_2 */ rowcr_tab 
   set id_uniq = -id_uniq, ts=localtimestamp
 where id_uniq in (-678, 678)   --id_uniq+0 avoids multi evaluation of filter
   and row_sleep(id_uniq, 10) = id_uniq+0;  

commit;

alter session set events '10046 trace name context off';  


Test Output



---========== Session-1 Run @T1, id_uniq changed from 678 to -678 ==========---
TESTDB(111)> update /*+ GATHER_PLAN_STATISTICS MONITOR Upd_1 */ rowcr_tab 
                set id_uniq = -id_uniq, ts=localtimestamp
              where id_uniq in (-678, 678)   --id_uniq+0 avoids multi evaluation of filter
                and row_sleep(id_uniq, 10) = id_uniq+0;  
                           
     Sleeping seconds: 10 for id: 678 at 25-JAN-2020 15:04:53

---========== Session-2 Run @T2, id_uniq first Consistency Read is 678, changed from -678 to 678 ==========---
TESTDB(222)> update /*+ GATHER_PLAN_STATISTICS MONITOR Upd_2 */ rowcr_tab 
                set id_uniq = -id_uniq, ts=localtimestamp
              where id_uniq in (-678, 678)   --id_uniq+0 avoids multi evaluation of filter
                and row_sleep(id_uniq, 10) = id_uniq+0;  
                
     Sleeping seconds: 10 for id: 678 at 25-JAN-2020 15:04:59
     Sleeping seconds: 10 for id: -678 at 25-JAN-2020 15:05:09
     Sleeping seconds: 10 for id: -678 at 25-JAN-2020 15:05:19
We can see that Session-1 shows one single line: "Sleeping seconds: 10 for id: 678".

But Session-2 shows three lines, one is "Sleeping seconds: 3 for id: 678", which is the first Consistency Read; the other two are "Sleeping seconds: 3 for id: -678", which are the updated result of Session-1 and re-read because of update restart. Each of them are with an inteval of 10 seconds. The three output lines by function row_sleep exactly reflect the first initial "update (rollbacked)", the second promoted "select for update", and the third "final update".

With this new approach, we can track all three update starts, to be precise, three update select phases. Whereas with "before row trigger", it is only fired two times, hence two pairs of output.

In the following SQL Monitoring Report and SQL Trace, Session-2 shows Execs=3 or starts=3 for row source "UPDATE". sqlmon_restarts.sql lists SQL_ID=fsg7u662gr5kj with starts=3.


SQL Monitoring Report



--------------- Session-1 ---------------
select SYS.DBMS_SQLTUNE.REPORT_SQL_MONITOR('grvrt3ux6xhpd', report_level=>'ALL', type=>'TEXT') from dual;
  update /*+ GATHER_PLAN_STATISTICS MONITOR Upd_1 */ rowcr_tab set id_uniq = -id_uniq, ts=localtimestamp where id_uniq in (-678, 678) --id_uniq+0 avoids multi evaluation of filter and row_sleep(id_uniq, 10) = id_uniq+0

========================================================================================================================================
| Id |       Operation       |       Name        |  Rows   | Cost |   Time    | Start  | Execs |   Rows   | Activity | Activity Detail |
|    |                       |                   | (Estim) |      | Active(s) | Active |       | (Actual) |   (%)    |   (# samples)   |
========================================================================================================================================
|  0 | UPDATE STATEMENT      |                   |         |      |         1 |    +10 |     1 |        0 |          |                 |
|  1 |   UPDATE              | ROWCR_TAB         |         |      |         1 |    +10 |     1 |        0 |          |                 |
|  2 |    INLIST ITERATOR    |                   |         |      |         1 |    +10 |     1 |        1 |          |                 |
|  3 |     INDEX UNIQUE SCAN | ROWCR_TAB#ID_UNIQ |       1 |    1 |         1 |    +10 |     2 |        1 |          |                 |
========================================================================================================================================

--------------- Session-2 ---------------
select SYS.DBMS_SQLTUNE.REPORT_SQL_MONITOR('bd0x94n13aqsc', report_level=>'ALL', type=>'TEXT') from dual;
  update /*+ GATHER_PLAN_STATISTICS MONITOR Upd_2 */ rowcr_tab set id_uniq = -id_uniq, ts=localtimestamp where id_uniq in (-678, 678) --id_uniq+0 avoids multi evaluation of filter and row_sleep(id_uniq, 10) = id_uniq+0

========================================================================================================================================
| Id |       Operation       |       Name        |  Rows   | Cost |   Time    | Start  | Execs |   Rows   | Activity | Activity Detail |
|    |                       |                   | (Estim) |      | Active(s) | Active |       | (Actual) |   (%)    |   (# samples)   |
========================================================================================================================================
|  0 | UPDATE STATEMENT      |                   |         |      |        11 |    +20 |     3 |        1 |          |                 |
|  1 |   UPDATE              | ROWCR_TAB         |         |      |        11 |    +20 |     3 |        1 |          |                 |
|  2 |    INLIST ITERATOR    |                   |         |      |        11 |    +20 |     3 |        3 |          |                 |
|  3 |     INDEX UNIQUE SCAN | ROWCR_TAB#ID_UNIQ |       1 |    1 |        21 |    +10 |     6 |        3 |          |                 |
========================================================================================================================================

   --  Predicate Information (identified by operation id):
   --  ---------------------------------------------------
   --     3 - access(("ID_UNIQ"=(-678) OR "ID_UNIQ"=678))
   --         filter("ROW_SLEEP"("ID_UNIQ",10)="ID_UNIQ"+0)       


SQL Trace



****************** Session-1 ******************
update /*+ GATHER_PLAN_STATISTICS MONITOR Upd_1 */ rowcr_tab
   set id_uniq = -id_uniq, ts=localtimestamp
 where id_uniq in (-678, 678)   --id_uniq+0 avoids multi evaluation of filter
   and row_sleep(id_uniq, 10) = id_uniq+0

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.02      10.02          0          4          6           1
Fetch        0      0.00       0.00          0          0          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        2      0.02      10.02          0          4          6           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         0 UPDATE  ROWCR_TAB (cr=4 pr=0 pw=0 time=10000668 us starts=1)
         1  INLIST ITERATOR  (cr=4 pr=0 pw=0 time=10000416 us starts=1)
         1   INDEX UNIQUE SCAN ROWCR_TAB#ID_UNIQ (cr=4 pr=0 pw=0 time=10000409 us starts=2 cost=1 size=15 card=1)(object id 4460833)

Elapsed times include waiting on following events:
  Event waited on                             Times   Max. Wait  Total Waited
  ----------------------------------------   Waited  ----------  ------------
  PL/SQL lock timer                               1        9.99          9.99

****************** Session-2 ******************
update /*+ GATHER_PLAN_STATISTICS MONITOR Upd_2 */ rowcr_tab
   set id_uniq = -id_uniq, ts=localtimestamp
 where id_uniq in (-678, 678)   --id_uniq+0 avoids multi evaluation of filter
   and row_sleep(id_uniq, 10) = id_uniq+0

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.05      30.05          0          9          8           1
Fetch        0      0.00       0.00          0          0          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        2      0.05      30.05          0          9          8           1

Rows (1st) Row Source Operation
---------- ---------------------------------------------------
         0 UPDATE  ROWCR_TAB (cr=9 pr=0 pw=0 time=30036737 us starts=3)
         3  INLIST ITERATOR  (cr=9 pr=0 pw=0 time=30002585 us starts=3)
         3   INDEX UNIQUE SCAN ROWCR_TAB#ID_UNIQ (cr=9 pr=0 pw=0 time=30002573 us starts=6 cost=1 size=15 card=1)(object id 4460833)

Elapsed times include waiting on following events:
  Event waited on                             Times   Max. Wait  Total Waited
  ----------------------------------------   Waited  ----------  ------------
  PL/SQL lock timer                               3       10.00         30.00


tpt-oracle/sqlmon_restarts.sql



--(https://github.com/tanelpoder/tpt-oracle/blob/master/sqlmon_restarts.sql) 
-- Copyright 2018 Tanel Poder. All rights reserved. More info at http://tanelpoder.com
-- Licensed under the Apache License, Version 2.0. See LICENSE.txt for terms & conditions.
-- Purpose:     List UPDATE/DELETE statements that have experienced restarts due to write consistency from V$SQL_PLAN_MONITOR

SELECT
    inst_id
  , sql_id
  , starts
  , sql_exec_start
  , sql_exec_id
  , plan_operation
  , plan_object_owner||'.'||plan_object_name object_name 
FROM 
    gv$sql_plan_monitor 
WHERE 
    plan_line_id = 1 
AND starts > 1
ORDER BY
    sql_id
  , sql_exec_start;

INST_ID  SQL_ID         STARTS SQL_EXEC_START        SQL_EXEC_ID  PLAN_OPERATION  OBJECT_NAME
-------  -------------  ------ --------------------  -----------  --------------  -----------
      1  bd0x94n13aqsc       3 25-JAN-2020 15:04:59     16777217  UPDATE          K.ROWCR_TAB


update deadlock


But continue to ask: "what happens if ...": if we modify two rows with one update statement in Session-1 (at first commit open transaction), but in Session-2, we immediately modify the same two rows with two update statements, at first id_uniq=101, then id_uniq=1, both sessions are hanging for a few seconds, then Session-1 (first updating session) throws a deadlock (all update statements are using INDEX UNIQUE SCAN ROWCR_TAB#ID_UNIQ).

---========== Session-1 Run @T3 ==========---
update rowcr_tab set ts=localtimestamp, seq=seq+row_sleep(1, 30) where id_uniq in (1, 101);

---========== Session-2 Run @T4 ==========---
update rowcr_tab set ts=localtimestamp, seq=seq+row_sleep(1, 30) where id_uniq in (101);
update rowcr_tab set ts=localtimestamp, seq=seq+row_sleep(1, 30) where id_uniq in (1);
Here the output:

---========== Session-1 Run @T3, Output==========---
19:03:03 TESTDB(111)> update rowcr_tab set ts=localtimestamp, seq=seq+row_sleep(1, 30) where id_uniq in (1, 101);
  Sleeping seconds: 30 for id: 1 at 25-JAN-2020 19:04:05
  update rowcr_tab set ts=localtimestamp, seq=seq+row_sleep(1, 30) where id_uniq in (1, 101)
                                            *
  ERROR at line 1:
  ORA-00060: deadlock detected while waiting for resource
  Elapsed: 00:00:36.17


---========== Session-2 Run @T4, Output ==========---
19:03:08 TESTDB(222)> update rowcr_tab set ts=localtimestamp, seq=seq+row_sleep(1, 30) where id_uniq in (101);
  Sleeping seconds: 30 for id: 1 at 25-JAN-2020 19:04:10
  1 row updated.
  Elapsed: 00:00:30.01
  
19:04:41 TESTDB(222)> update rowcr_tab set ts=localtimestamp, seq=seq+row_sleep(1, 30) where id_uniq in (1);
  Sleeping seconds: 30 for id: 1 at 25-JAN-2020 19:04:41
  1 row updated.
  Elapsed: 00:00:30.39

Monday, December 23, 2019

Oracle Database and Java Datetime Conversion Differences across Daylight Saving Time (DST)

Following the long discussions of Blog: Oracle Datetime (1) - Concepts, we also observed the difference of Datetime conversion on Daylight Saving Time (DST) between Java and Sql.

For example, for Paris Winter to Summer DST time switch, on Sunday 31 March 2019, 02:00:00 clocks were turned forward 1 hour to 03:00:00.

In this Blog, we made two tests to demonstrate the difference of Datetime arithmetic across DST turning point, one is in Java, another is in Sql.

Note: All tests are done in Oracle 12c, 18c, 19c and Java 8.


1. Java Test


Create following Java class:

------------------ Java Code -----------------

import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class TestDST
{
   private static final String DATE_FORMAT = "yyyy-MMMM-dd kk:mm:ss VV O";
   private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
    
   public static void main(String[] args)
   {
      ZoneId timeZone = ZoneId.of("Europe/Paris");    // timezone
      LocalDateTime localDT = LocalDateTime.of(2019, Month.APRIL, 01, 02, 11, 33);  //2019-04-01 02:11:33
      
      for (int h = 1; h <= 3; h++) {
        localDT = LocalDateTime.of(2019, Month.APRIL, 01, h, 11, 33);  
        ZonedDateTime zDT = localDT.atZone(timeZone);  //Zoned date time
        System.out.println("(" + formatter.format(zDT) + " - 1 Day) = " + formatter.format(zDT.minusDays(1))); 
        System.out.println("(" + formatter.format(zDT) + " - 2 Day) = " + formatter.format(zDT.minusDays(2)));
      }
      System.out.println();
      for (int h = 1; h <= 3; h++) {
        localDT = LocalDateTime.of(2019, Month.MARCH, 30, h, 11, 33);  
        ZonedDateTime zDT = localDT.atZone(timeZone);  //Zoned date time
        System.out.println("(" + formatter.format(zDT) + " + 1 Day) = " + formatter.format(zDT.plusDays(1))); 
        System.out.println("(" + formatter.format(zDT) + " + 2 Day) = " + formatter.format(zDT.plusDays(2)));
      }        
   }
}  
Compile and then run it:

javac -cp . TestDST.java

java -cp . TestDST
Here the Output for Datetime minus and plus of 1 or 2 days in Java.

$ > java -cp . TestDST

--------------------------------- Datetime minus ---------------------------------
(2019-April-01 01:11:33 Europe/Paris GMT+2 - 1 Day) = 2019-March-31 01:11:33 Europe/Paris GMT+1
(2019-April-01 01:11:33 Europe/Paris GMT+2 - 2 Day) = 2019-March-30 01:11:33 Europe/Paris GMT+1
(2019-April-01 02:11:33 Europe/Paris GMT+2 - 1 Day) = 2019-March-31 03:11:33 Europe/Paris GMT+2
(2019-April-01 02:11:33 Europe/Paris GMT+2 - 2 Day) = 2019-March-30 02:11:33 Europe/Paris GMT+1
(2019-April-01 03:11:33 Europe/Paris GMT+2 - 1 Day) = 2019-March-31 03:11:33 Europe/Paris GMT+2
(2019-April-01 03:11:33 Europe/Paris GMT+2 - 2 Day) = 2019-March-30 03:11:33 Europe/Paris GMT+1

--------------------------------- Datetime plus ---------------------------------
(2019-March-30 01:11:33 Europe/Paris GMT+1 + 1 Day) = 2019-March-31 01:11:33 Europe/Paris GMT+1
(2019-March-30 01:11:33 Europe/Paris GMT+1 + 2 Day) = 2019-April-01 01:11:33 Europe/Paris GMT+2
(2019-March-30 02:11:33 Europe/Paris GMT+1 + 1 Day) = 2019-March-31 03:11:33 Europe/Paris GMT+2
(2019-March-30 02:11:33 Europe/Paris GMT+1 + 2 Day) = 2019-April-01 02:11:33 Europe/Paris GMT+2
(2019-March-30 03:11:33 Europe/Paris GMT+1 + 1 Day) = 2019-March-31 03:11:33 Europe/Paris GMT+2
(2019-March-30 03:11:33 Europe/Paris GMT+1 + 2 Day) = 2019-April-01 03:11:33 Europe/Paris GMT+2


2. Sql Test


Here the test and output for the identical Datetime minus and plus of 1 or 2 days in Oracle Sql.

Sql > column datetime format a100
Sql > alter session set time_zone = 'Europe/Paris';

Sql > with base as (select timestamp'2019-04-01 01:11:00 Europe/Paris' v from dual)
          ,diff as (select level-1 v from dual connect by level <= 3)
      select '('||(base.v+numtodsinterval(hh.v, 'hour'))||' - '||dd.v||' day) = '||
              ((base.v+numtodsinterval(hh.v, 'hour')) - numtodsinterval(dd.v, 'day')) datetime 
        from base, diff hh, diff dd
      where dd.v > 0
      order by base.v, hh.v, dd.v; 

--------------------------------- Datetime minus ---------------------------------
(01-APR-2019 01:11:00 EUROPE/PARIS - 1 day) = 31-MAR-2019 00:11:00 EUROPE/PARIS
(01-APR-2019 01:11:00 EUROPE/PARIS - 2 day) = 30-MAR-2019 00:11:00 EUROPE/PARIS
(01-APR-2019 02:11:00 EUROPE/PARIS - 1 day) = 31-MAR-2019 01:11:00 EUROPE/PARIS
(01-APR-2019 02:11:00 EUROPE/PARIS - 2 day) = 30-MAR-2019 01:11:00 EUROPE/PARIS
(01-APR-2019 03:11:00 EUROPE/PARIS - 1 day) = 31-MAR-2019 03:11:00 EUROPE/PARIS
(01-APR-2019 03:11:00 EUROPE/PARIS - 2 day) = 30-MAR-2019 02:11:00 EUROPE/PARIS


Sql > with base as (select timestamp'2019-03-30 01:11:00 Europe/Paris' v from dual)
          ,diff as (select level-1 v from dual connect by level <= 3)
      select '('||(base.v+numtodsinterval(hh.v, 'hour'))||' + '||dd.v||' day) = '||
              ((base.v+numtodsinterval(hh.v, 'hour')) + numtodsinterval(dd.v, 'day')) datetime 
        from base, diff hh, diff dd
      where dd.v > 0
      order by base.v, hh.v, dd.v; 

--------------------------------- Datetime plus ---------------------------------
(30-MAR-2019 01:11:00 EUROPE/PARIS + 1 day) = 31-MAR-2019 01:11:00 EUROPE/PARIS
(30-MAR-2019 01:11:00 EUROPE/PARIS + 2 day) = 01-APR-2019 02:11:00 EUROPE/PARIS
(30-MAR-2019 02:11:00 EUROPE/PARIS + 1 day) = 31-MAR-2019 03:11:00 EUROPE/PARIS
(30-MAR-2019 02:11:00 EUROPE/PARIS + 2 day) = 01-APR-2019 03:11:00 EUROPE/PARIS
(30-MAR-2019 03:11:00 EUROPE/PARIS + 1 day) = 31-MAR-2019 04:11:00 EUROPE/PARIS
(30-MAR-2019 03:11:00 EUROPE/PARIS + 2 day) = 01-APR-2019 04:11:00 EUROPE/PARIS


3. Java vs. Sql


From above output, we can see:


3.1. Java Arithmetic


Java is trying to maintain Datetime literal string value as much as possible. for example,
  (2019-April-01 01:11:33 Europe/Paris GMT+2 - 1 Day) = 2019-March-31 01:11:33 Europe/Paris GMT+1
it only shift one day back, even though the absolute time
  from 2019-March-31 01:11:33 Europe/Paris GMT+1
  to   2019-April-01 01:11:33 Europe/Paris GMT+2
is 23 hours, not one full day of 24 hours. If Datetime value not exists in that timezone, round to the nearest higher value (Positive Infinity). for example,
  (2019-April-01 02:11:33 Europe/Paris GMT+2 - 1 Day) = 2019-March-31 03:11:33 Europe/Paris GMT+2
since "2019-March-31 02:11:33 Europe/Paris GMT+2" does not exist.


3.2. Sql Arithmetic


Sql performs strict 24 hours per day calculation. For example,
  (01-APR-2019 01:11:00 EUROPE/PARIS - 1 day) = 31-MAR-2019 00:11:00 EUROPE/PARIS
If Datetime value does not exist in that timezone, for example, to minus one day, it maps to exact timestamp 24 hours ago.
  (01-APR-2019 02:11:00 EUROPE/PARIS - 1 day) = 31-MAR-2019 01:11:00 EUROPE/PARIS


3.3. Result Differences


Now we end up with different result between Java and Sql, the first one even in different Time Zone (GMT+2 vs. GMT+1).

Java: (2019-April-01 02:11:33 Europe/Paris GMT+2 - 1 Day) = 2019-March-31 03:11:33 Europe/Paris GMT+2
Sql:  (  01-APR-2019 02:11:00 EUROPE/PARIS - 1 day)       =   31-MAR-2019 01:11:00 EUROPE/PARIS

Java: (2019-April-01 01:11:33 Europe/Paris GMT+2 - 2 Day) = 2019-March-30 01:11:33 Europe/Paris GMT+1
Sql:  (  01-APR-2019 01:11:00 EUROPE/PARIS - 2 day)       =   30-MAR-2019 00:11:00 EUROPE/PARIS

Oracle 19.4 OracleJVM JAVA_JIT_ENABLED Not Working on AIX

Java run-time compilation, JAVA_JIT_ENABLED (JIT), was first introduced as a performance upgrade in JDK 1.1.6, and now is a standard tool invoked whenever Java is used. It enables JVM well exploit the features of the computer hardware and operating system (OS).

On AIX, when calling Oracle 19.4 Java stored procedure, which is running on server-side integrated OracleJVM, JIT enabled is not working, it behaves same as disabled. If the Java stored procedure is invoking system dependent native compiled Java classes (big endian), it falls back to the default OracleJVM provided Java classes (little endian), hence performance degradation.

In this Blog, we first demonstrate this behaviour with JarInputStream to import signed Jar file with JIT enabled (Oracle Doc: For platforms that support the JIT compiler, the default value of this parameter is true). Then we reveal the bug by investigating the call stacks, and finally present a workaround.

A JAR file is essentially a Zip file that contains an optional META-INF directory. If a manifest is present under META-INF directory (security signatures) and verify is true (default), JarInputStream attempts to verify each file for the signed Jar. JarInputStream and JarOutputStream classes extend to ZipInputStream and ZipOutputStream
(For JAR File, see JAR File Specification
For Security Provider, see Java Cryptography Architecture (JCA) Reference Guide).

Update (07 Feb. 2020): The bug should be fixed in Patch 29707582: EC.C COMPILE FAILURE.


1. Test Setup


Here the complete steps and code to reproduce the issue.

------ 1. Create a Jar file under directory /tmp ------
$> jar -cvf testJar.jar test1.txt

------ 2. List Jar content ------
$>  unzip -l testJar.jar

  Archive:  testJar.jar
    Length      Date    Time    Name
  ---------  ---------- -----   ----
        144  12-01-2019 08:27   META-INF/MANIFEST.MF
        306  12-01-2019 08:27   META-INF/KUNALIAS.SF
       1471  12-01-2019 08:27   META-INF/KUNALIAS.DSA
          0  12-01-2019 08:26   META-INF/
  110947240  12-01-2019 08:23   test1.txt
  ---------                     -------
  110949161                     5 files
       
------ 3. Create a key ------
$> keytool -genkey -alias kunalias1 -keystore kunkey1      

------ 4. Sign Jar file ------
$> jarsigner -keystore kunkey1 -storepass kunkey1 -keypass kunkey1 testJar.jar kunalias1
  
------ 5. Create a DB table to store Jar entries, and a Plsql row insert procedure to be called by Java ------

create table test_blob_tab(jar_name varchar2(100), jar_entry blob, sts timestamp with time zone);

create or replace function insert_blob(p_jar_name varchar2, p_jar_entry blob) return number as
  l_rowcount integer;
begin
  insert into test_blob_tab values (p_jar_name, p_jar_entry, systimestamp);
  l_rowcount := sql%rowcount;
  commit;
  return l_rowcount;
end;
/

------ 6. Create Java stored procedure OracleJVMJarInputStream.java  ------
(see Blog appended code in Section 8 OracleJVM Test Code).
     
------ 7. Publishing the Java Java stored procedure to Plsql  ------

create or replace procedure OracleJVMJarInputStream(p_verify varchar2, p_info varchar2) as language java
name 'OracleJVMJarInputStream.run(java.lang.String, java.lang.String)';
/


2. Test Run


We will make two tests, one with signature verify, another without signature verify. The test shows that signature verify takes 34 minutes, but another 12 seconds.


2.1. Invoke OracleJVMJarInputStream with signature verify



Sql > set serveroutput on size 50000
Sql > exec dbms_java.set_output(50000); 

Sql > exec OracleJVMJarInputStream('true', 'no');

      ********* getNextJarEntry *********
      ------ NextJarEntry: 1, Name: META-INF/KUNALIAS.SF ------
               getNextEntry ElapsedMills: 2, at: 1576576740110
               Insert DB 1 row, blob size 0 at Sun Dec 01 10:59:00 CET 2019
               readContent ElapsedMills: 20, at: 1576576740130
      ------ NextJarEntry: 2, Name: META-INF/KUNALIAS.DSA ------
               getNextEntry ElapsedMills: 0, at: 1576576740130
               Insert DB 1 row, blob size 0 at Sun Dec 01 10:59:06 CET 2019
               readContent ElapsedMills: 6440, at: 1576576746571
      ------ NextJarEntry: 3, Name: META-INF/ ------
               getNextEntry ElapsedMills: 0, at: 1576576746572
               Insert DB 1 row, blob size 0 at Sun Dec 01 10:59:06 CET 2019
               readContent ElapsedMills: 16, at: 1576576746588
      ------ NextJarEntry: 4, Name: test1.txt ------
               getNextEntry ElapsedMills: 1, at: 1576576746589
               Insert DB 1 row, blob size 110920480 at Sun Dec 01 11:33:01 CET 2019
               readContent ElapsedMills: 2034693, at: 1576578781282
       
      Elapsed: 00:34:01.35


2.2. Invoke OracleJVMJarInputStream without signature verify



Sql > exec OracleJVMJarInputStream('false', 'no');

      ********* getNextJarEntry *********
      ------ NextJarEntry: 1, Name: META-INF/KUNALIAS.SF ------
               getNextEntry ElapsedMills: 2, at: 1576588511208
               Insert DB 1 row, blob size 0 at Sun Dec 01 14:15:11 CET 2019
               readContent ElapsedMills: 18, at: 1576588511226
      ------ NextJarEntry: 2, Name: META-INF/KUNALIAS.DSA ------
               getNextEntry ElapsedMills: 0, at: 1576588511226
               Insert DB 1 row, blob size 0 at Sun Dec 01 14:15:11 CET 2019
               readContent ElapsedMills: 10, at: 1576588511237
      ------ NextJarEntry: 3, Name: META-INF/ ------
               getNextEntry ElapsedMills: 0, at: 1576588511237
               Insert DB 1 row, blob size 0 at Sun Dec 01 14:15:11 CET 2019
               readContent ElapsedMills: 10, at: 1576588511247
      ------ NextJarEntry: 4, Name: test1.txt ------
               getNextEntry ElapsedMills: 0, at: 1576588511248
               Insert DB 1 row, blob size 110920480 at Sun Dec 01 14:15:23 CET 2019
               readContent ElapsedMills: 12271, at: 1576588523519
      
      Elapsed: 00:00:12.44


3. Reasoning


The above two tests showed a factor of 170 difference (34 minutes vs. 12 seconds) between Jar import with vs. without signature verify. For each Jar entry of size n, the complexity seems non-linear to its size (O(n^2) or higher). When reading Jar archived big sized entries, the performance degradation becomes dramatic.


3.1. Code Path


Open two Sqlplus sessions. In the first session with (sid, serial#) = (101, 1010), we execute again import with signature verify:

Sql > set serveroutput on size 50000
Sql > exec dbms_java.set_output(50000); 
Sql > alter session set tracefile_identifier = 'java_dump_1';  
Sql > exec OracleJVMJarInputStream('true', 'no');
In the second session, we make a few Java stack dump:

Sql > begin
       for i in 1..600 loop
         sys.dbms_java_dump.dump(sys.dbms_java_dump.java_dump_stack, 101, 1010);
         dbms_lock.sleep(0.1);
       end loop;
      end;
      /
Now if we open the dump file, we can see most of them having call stacks like:

*** 2019-12-01T13:34:24.768993+01:00
*** Java stack trace for the active thread
  at sun.security.provider.ByteArrayAccess.b2iBig(ByteArrayAccess.java:247)
  at sun.security.provider.ByteArrayAccess.b2iBig64(ByteArrayAccess.java:309)
  at sun.security.provider.ByteArrayAccess.b2iBig(ByteArrayAccess.java:256)
  at sun.security.provider.ByteArrayAccess.b2iBig64(ByteArrayAccess.java:309)
  at sun.security.provider.SHA2.implCompress(SHA2.java:196)
  at sun.security.provider.DigestBase.implCompressMultiBlock(DigestBase.java:141)
  at sun.security.provider.DigestBase.engineUpdate(DigestBase.java:128)
  at java.security.MessageDigest$Delegate.engineUpdate(MessageDigest.java:584)
  at java.security.MessageDigest.update(MessageDigest.java:325)
  at sun.security.util.ManifestEntryVerifier.update(ManifestEntryVerifier.java:173)
  at java.util.jar.JarVerifier.update(JarVerifier.java:227)
  at java.util.jar.JarInputStream.read(JarInputStream.java:212)
  at java.util.zip.ZipInputStream.closeEntry(ZipInputStream.java:140)
  at java.util.zip.ZipInputStream.getNextEntry(ZipInputStream.java:118)
  at java.util.jar.JarInputStream.getNextEntry(JarInputStream.java:142)
  at java.util.jar.JarInputStream.getNextJarEntry(JarInputStream.java:179)
  at OracleJVMJarInputStream.run(OracleJVMJarInputStream:49)


3.2. Java Class: sun.security.provider.ByteArrayAccess


If we look the Java 8 ByteArrayAccess source delivered by sun.security.provider. The document states that the method is optimized for little endian, and big endian, such as SPARC.

Sticking to above call stack, we can see that sun.security.provider.DigestBase calls big endian specific b2iBig64 for algorithm SHA2, and it is further implemented by b2iBig, which contains a while loop.

/**
 * Optimized methods for converting between byte[] and int[]/long[], both for
 * big endian and little endian byte orders.
 *
 * Currently, it includes a default code path plus two optimized code paths.
 * One is for little endian architectures that support full speed int/long
 * access at unaligned addresses (i.e. x86/amd64). The second is for big endian
 * architectures (that only support correctly aligned access), such as SPARC.
 * These are the only platforms we currently support, but other optimized
 * variants could be added as needed.
 */
final class ByteArrayAccess {
...
          /**
           * byte[] to int[] conversion, big endian byte order.
           */
      static void b2iBig(byte[] in, int inOfs, int[] out, int outOfs, int len) {
      ...
             while (inOfs < len) {
       
      
          // Special optimization of b2iBig(in, inOfs, out, 0, 64)
      static void b2iBig64(byte[] in, int inOfs, int[] out) { 
      ...
             b2iBig(in, inOfs, out, 0, 64);


3.3. Security Provider


If we list all Security Providers in OracleJVM:

Sql > exec OracleJVMJarInputStream('true', 'info');

      ===============0. Provider: SUN version 1.8
        ----- 97. Element:MessageDigest.SHA-256 
      ===============1. Provider: SunRsaSign version 1.8
      ===============2. Provider: SunJSSE version 1.8
      ===============3. Provider: SunJCE version 1.8
      ===============4. Provider: SunJGSS version 1.8
      ===============5. Provider: SunSASL version 1.8
      ===============6. Provider: XMLDSig version 1.8
      ===============7. Provider: SunPCSC version 1.8
we can see "MessageDigest.SHA-256" (for SHA2) is provided by "SUN version 1.8". No any IBM Security Provider is listed.


4. Deep into JIT Compiled Native Code


Now we can have a further look of the allocated shared memory segments from UNIX.


4.1. JIT Compiled Native Code


JIT run-time Compiled Native Code are dynamically allocated in shared memory, and can be monitored by ipcs command. If we compare the following ipcs output on shared memory allocated for JIT Compiled Native Code between not working Oracle 19.4 and that of properly working Oracle 19.3:

---=============== Oracle 19.4 on AIX, JIT not Working ===============---
$> ipcs -ar
T        ID KEY        MODE        OWNER   GROUP CREATOR CGROUP NATTCH SEGSZ  CPID LPID ATIME  DTIME   CTIME  RTFLAGS NAME
Shared Memory:
m         - 0xffffffff --rw-r----- oracle  dba   oracle  dba    0      0 30146690  0 no-entry no-entry 15:28:51  -    /JOEZSHM_testdb194_1_0_1_0_0_2971349679
m         - 0xffffffff --rw-r----- oracle  dba   oracle  dba    0      0 18219016  0 no-entry no-entry 15:28:43  -    /JOEZSHM_testdb194_1_0_0_0_0_1989310950
m         - 0xffffffff --rw-r----- oracle  dba   oracle  dba    0      0 20185178  0 no-entry no-entry 14:23:14  -    /JOEZSHM_testdb194_1_0_1_0_0_2979596769

---=============== Oracle 19.3 on AIX, JIT works ===============---

$> ipcs -ar
T        ID KEY        MODE        OWNER  GROUP CREATOR CGROUP NATTCH SEGSZ    CPID     LPID  ATIME    DTIME   CTIME   RTFLAGS NAME
Shared Memory:
m         - 0xffffffff --rw-rw---- oracle dba   oracle  dba      0    16777216 31588636 0    no-entry no-entry 13:30:56   -    /JOEZSHM_testdb193_1_0_1_0_0_2979282907
m         - 0xffffffff --rw-rw---- oracle dba   oracle  dba      0     8388608 31588636 0    no-entry no-entry  8:10:45   -    /JOEZSHM_testdb193_1_0_1_1_0_2979282907
m         - 0xffffffff --rw-rw---- oracle dba   oracle  dba      0    16777216 34931180 0    no-entry no-entry 12:27:51   -    /JOEZSHM_testdb193_1_0_0_0_0_1989371893
we can see two differences, the first is the permissions; the second is shared segment size:

  Oracle 19.4 MODE   --rw-r----- 
  Oracle 19.3 MODE   --rw-rw----  
  
  Oracle 19.4 SEGSZ  0 
  Oracle 19.3 SEGSZ  not 0, one is with 8388608 (8MB), and two are 16MB 16777216(16MB)  
Later we will see that MZ00 process trace file constantly shows error message: "Unable to allocate code space", probably because of insufficient permissions and 0 sized shared segments in Oracle 19.4.

Since enabling JIT is not working on AIX (big endian) for Oracle 19.4, it falls back to the default OracleJVM provided Java classes (little endian), hence performance degradation.

As a small test, we can also create a shared segment with

  SEGSZ: 1024 bytes, and MODE: "--rw-r-----" 
as follows: (see IBM Doc: shmget–Allocate shared memory)

#include <sys/ipc.h>
#include <sys/shm.h>
int main()

{
 key_t key;
 int   i;

 key = ftok("/usr/testfile2", 2);
 i= shmget(key, 1024, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP);
 printf("shmget key: %i\n", i);
 }
 
-- compile and run compiled c code
 
$ > ipcs -ar
T        ID     KEY        MODE        OWNER  GROUP CREATOR CGROUP NATTCH SEGSZ CPID    LPID   ATIME    DTIME  CTIME  RTFLAGS NAME
Shared Memory:
m 329257579     0xffffffff --rw-r----- oracle dba   oracle  dba    0      1024  29229194   0 no-entry no-entry 9:17:54


4.2. Oracle JAVAVM JIT Compiler Slave MZ00


According to Oracle Doc, JIT compiler runs as an MMON slave, in a single background process MZ00 for the DB instance. While the JIT compiler is running and actively compiling methods, we may see this background process consuming CPU and memory resources equivalent to an active user Java session.

In fact, above Oracle 19.4 ipcs output shows that CPID (creator process ID) is 20185178, which is the UNIX process id of MZ00.

Open Oracle 19.4 MZ00 trace file: aix194db_mz00_20185178.trc, we can see the failed message: (turn on 10046 Event Trace on MZ00 can observe more activities)

Unix process pid: 20185178, image: oracle@aix194db (MZ00)

*** 2019-12-01T22:23:14.257628+01:00
*** SERVICE NAME:(SYS$BACKGROUND) 2019-12-01T22:23:14.257687+01:00
*** MODULE NAME:(MMON_SLAVE) 2019-12-01T22:23:14.257699+01:00
*** ACTION NAME:(JAVAVM JIT slave action) 2019-12-01T22:23:14.257711+01:00


*** 2019-12-01T22:23:14.257754+01:00
JIT running
joez_shm_open_object failed: size = 16777216, extnam = /JOEZSHM_testdb194_1_0_1_0_0_2979596769  flags = 0x34 
joez: Failed loading machine code: Unable to allocate code space
Done compiling java/lang/SecurityManager$1.run

*** 2019-12-01T22:23:26.185994+01:00
joez_shm_open_object failed: size = 16777216, extnam = /JOEZSHM_testdb194_1_0_0_0_0_1989310950  flags = 0x34 
joez: Failed loading machine code: Unable to allocate code space
Done compiling oracle/aurora/rdbms/EnvironmentSpecificImpl.securityManagerImpl
The above output shows that each joez_shm_open_object is indentified by "extnam", which maybe indicates that Java launcher uses Java Extension path to find classes.

However if we search Bootstrap classes in rt.jar, we can see IBM security provider and ibm SHA2 algorithm.

unzip -l $ORACLE_HOME/jdk/jre/lib/rt.jar  
       0  02-07-2019 17:37   com/ibm/security/bootstrap/
     3339  10-30-2018 16:47   com/ibm/security/bootstrap/SHA2.class    
The above "flags" value "0x34" is ASCII "4", which could signify GroupMode "r--" in above ipcs output.

If we truss MZ00 JIT slave, it shows constantly "Err#22 EINVAL". According to AIX Doc, lseek Error Codes: EINVAL stands for:

The resulting offset would be greater than the maximum offset allowed for the file or device associated with FileDescriptor. The lseek subroutine was used with a file descriptor obtained from a call to the shm_open subroutine.

Since Oracle 19.4 created shared segments are 0 sized, MZ00 throws above offset error.

$> truss -dp 20185178
Sun Dec 01 22:25:31 2019
0.0000:        thread_post(50135221)            = 0
0.0009:        shm_open(0x0FFFFFFFFFFE4590, 258, 504) = 3606
0.0013:        lseek(3606, 0, 1)                Err#22 EINVAL
0.0018:        lseek(3259, 0, 1)                = 701432
0.0072:        kwrite(3259, "\n * * *   2 0 1 9 - 1 2".., 38) = 38
0.0075:        kwrite(3505, " ! 0 e 4 c\n", 6)  = 6
0.0078:        lseek(3259, 0, 1)                = 701470
0.0080:        kwrite(3259, " j o e z _ s h m _ o p e".., 108) = 108
0.0083:        kwrite(3505, " J ? g T y P ~ 0 i 1\n", 11) = 11
0.0086:        kwrite(3259, "\n", 1)            = 1
0.0089:        kwrite(3259, " j o e z :   F a i l e d".., 64) = 64
0.0092:        kwrite(3259, "\n", 1)            = 1
0.0172:        kwrite(3259, " D o n e   c o m p i l i".., 77) = 77
0.0174:        kwrite(3259, "\n", 1)            = 1
0.0178:        kwrite(3259, " C o m p i l i n g   c o".., 119) = 119
0.0181:        kwrite(3259, "\n", 1)            = 1
1.3517:        thread_post(50135221)            = 0
1.3574:        shm_open(0x0FFFFFFFFFFE4590, 258, 504) = 3607
1.3578:        lseek(3607, 0, 1)                Err#22 EINVAL     


5. Reproduce In Lower Oracle Versions


The above observed behaviour shows the JIT enabled is same as disabled in Oracle 19.4. So we can try to disable JIT in Oracle versions before 19.4 on AIX, and check if it is reproducible (following tests are done in Oracle 12c, 18c, 19.3 on AIX).


5.1. JIT enabled Test


Take one oracle 12c DB on AIX with JAVA_JIT_ENABLED enabled, run the same test with signature verify, it takes about 11 seconds, compared to above 34 minutes from Oracle 19.4.

Sql > set serveroutput on size 50000
Sql > exec dbms_java.set_output(50000); 
Sql > alter session set tracefile_identifier = 'java_dump_1';  
Sql > exec OracleJVMJarInputStream('true', 'no');

  ********* getNextJarEntry *********
  ------ NextJarEntry: 1, Name: META-INF/KUNALIAS.SF ------
           getNextEntry ElapsedMills: 70, at: 1576667113846
           Insert DB 1 row, blob size 0 at Sun Dec 01 12:05:14 CET 2019
           readContent ElapsedMills: 333, at: 1576667114180
  ------ NextJarEntry: 2, Name: META-INF/KUNALIAS.DSA ------
           getNextEntry ElapsedMills: 0, at: 1576667114180
           Insert DB 1 row, blob size 0 at Sun Dec 01 12:05:14 CET 2019
           readContent ElapsedMills: 504, at: 1576667114684
  ------ NextJarEntry: 3, Name: META-INF/ ------
           getNextEntry ElapsedMills: 0, at: 1576667114685
           Insert DB 1 row, blob size 0 at Sun Dec 01 12:05:14 CET 2019
           readContent ElapsedMills: 2, at: 1576667114687
  ------ NextJarEntry: 4, Name: test1.txt ------
           getNextEntry ElapsedMills: 3, at: 1576667114690
           Insert DB 1 row, blob size 110920480 at Sun Dec 01 12:05:24 CET 2019
           readContent ElapsedMills: 10333, at: 1576667125023
  
  Elapsed: 00:00:11.51


5.2. JIT disabled Test


Now we remove all JIT compiled native code from the same Oracle 12c DB, disable JIT, re-run above test (For details, see Blog: Remove Stale Native Code Files on AIX).

------ 1. List JIT compiled native code ------ 

$ > ipcs -ar |grep -e JOXSHM_EXT | awk '{cnt+=1; sum+=$10} END {print "Count=",cnt,"Sum=",sum,"Average=",sum/cnt}'
    Count= 1918 Sum= 24076288 Average= 12552.8
      
------ 2. Remove JIT compiled native code ------ 

$ > ipcs -ar |grep -e JOXSHM_EXT | awk ' {name = $17; cmd = "ipcrm -r -m " name; print "Run ", cmd; system( cmd )}'

------ 3. Disable java_jit_enabled ------ 

Sql > alter system set java_jit_enabled = false scope=both;

------ 4. Restart DB ------ 

Sql > startup force

------ 5. Slow Test: 16 minutes ------ 

Sql > exec OracleJVMJarInputStream('true', 'no');
  ********* getNextJarEntry *********
  ------ NextJarEntry: 1, Name: META-INF/KUNALIAS.SF ------
           getNextEntry ElapsedMills: 72, at: 1576667818306
           Insert DB 1 row, blob size 0 at Sun Dec 01 12:16:58 CET 2019
           readContent ElapsedMills: 872, at: 1576667819179
  ------ NextJarEntry: 2, Name: META-INF/KUNALIAS.DSA ------
           getNextEntry ElapsedMills: 1, at: 1576667819180
           Insert DB 1 row, blob size 0 at Sun Dec 01 12:17:02 CET 2019
           readContent ElapsedMills: 3456, at: 1576667822636
  ------ NextJarEntry: 3, Name: META-INF/ ------
           getNextEntry ElapsedMills: 1, at: 1576667822637
           Insert DB 1 row, blob size 0 at Sun Dec 01 12:17:02 CET 2019
           readContent ElapsedMills: 3, at: 1576667822640
  ------ NextJarEntry: 4, Name: test1.txt ------
           getNextEntry ElapsedMills: 37, at: 1576667822677
           Insert DB 1 row, blob size 110920480 at Sun Dec 01 12:33:23 CET 2019
           readContent ElapsedMills: 980996, at: 1576668803674
  
  Elapsed: 00:16:27.43         

------ 6. Dump Java call stack of test session ------ 
    -- Open a second Sqlplus session, make a few Java stack dump:
  
Sql > begin
       for i in 1..600 loop
         sys.dbms_java_dump.dump(sys.dbms_java_dump.java_dump_stack, 101, 1010);
         dbms_lock.sleep(0.1);
       end loop;
      end;
      /
 
    -- The call stack shows similar sun.security.provider for invoked SHA2 algorithm:
    --   sun.security.provider.SHA2.implCompress(SHA2.java:193)
    
------ 7. List JIT compiled native code ------ 

$ > ipcs -ar |grep -e JOXSHM_EXT

    -- JIT disable, no row returned, no shared memory segment created
The above test in Oracle 12c DB shows it takes about 16 minutes when JIT disabled, compared to above 11 seconds with JIT enabled. In case of JIT disabled, no shared memory segments created, Oracle JVM can not benefit from computer hardware and operating system (OS) dependent optimized JAVA classes. Therefore the same behaviour can be reproduced if JIT is disabled in Oracle versions lower than 19.4.


5.3. Shared Memory Segment for JIT compiled native code


We also observed changes of JIT compiled Shared Memory Segment naming and size following Oracle versions.

 
--- Oracle 12c JIT compiled Shared Memory Segment naming ---

  /JOXSHM_EXT_420_testdb12c_11534967

--- Oracle 18c JIT compiled Shared Memory Segment naming ---
  /JOEZSHM_testdb18c_1_0_32075_0_0_1164116801
  
--- Oracle 19c JIT compiled Shared Memory Segment naming ---
  /JOEZSHM_testdb194_1_0_1_0_0_3031177879
Till 18c, there are about 1000 JIT compiled Shared Memory Segments with each around 10KB, probably each one represents one Java class.

From 19c, there are only a few (less than 10) JIT compiled Shared Memory Segments with each around 8MB or 16MB, probably they are now grouped under Java Packages.


6. Workaround


As a workaround, we can use standard Java JVM to import Jar files into DB by JDBC. Besides fixing the performance problem, one more advantage is that the workaround can be called from any other platforms, or even little endian machine (See appended code in Section: "9. Standard Java JVM Test Code").

Here is one test run, which takes about 25 seconds.

$ > $ORACLE_HOME/jdk/bin/java -cp $ORACLE_HOME/jdbc/lib/ojdbc8.jar:. \
                                  JavaJVMJarInputStream \
                                 "jdbc:oracle:thin:k/s@testdb194:1522:testdb194" true  no

  ********* getNextJarEntry *********
  ------ NextJarEntry: 1, Name: META-INF/KUNALIAS.SF ------
           getNextEntry ElapsedMills: 1, at: 1576670454217
           Insert DB 1 row, blob size 0 at Sun Dec 01 13:00:55 CET 2019
           readContent ElapsedMills: 1116, at: 1576670455334
  ------ NextJarEntry: 2, Name: META-INF/KUNALIAS.DSA ------
           getNextEntry ElapsedMills: 0, at: 1576670455335
           Insert DB 1 row, blob size 0 at Sun Dec 01 13:00:55 CET 2019
           readContent ElapsedMills: 151, at: 1576670455486
  ------ NextJarEntry: 3, Name: META-INF/ ------
           getNextEntry ElapsedMills: 0, at: 1576670455486
           Insert DB 1 row, blob size 0 at Sun Dec 01 13:00:55 CET 2019
           readContent ElapsedMills: 18, at: 1576670455504
  ------ NextJarEntry: 4, Name: test1.txt ------
           getNextEntry ElapsedMills: 0, at: 1576670455504
           Insert DB 1 row, blob size 110920480 at Sun Dec 01 13:01:18 CET 2019
           readContent ElapsedMills: 23346, at: 1576670478850
If we re-run the test with JMX option, and monitor it by Java VisualVM:

$ > $ORACLE_HOME/jdk/bin/java -cp $ORACLE_HOME/jdbc/lib/ojdbc8.jar:. \
                                 -Dcom.sun.management.jmxremote.port=1521 \
                                 -Dcom.sun.management.jmxremote.authenticate=false \
                                 -Dcom.sun.management.jmxremote.ssl=false \
                                  JavaJVMJarInputStream \
                                 "jdbc:oracle:thin:k/s@testdb194:1522:testdb194" true  no
From VisualVM, we make a few Thread Dump. The call stack shows that com.ibm.security.bootstrap provider is invoked for SHA2 algorithm. Here one Thread Dump:

2019-12-01 13:36:17
Full thread dump IBM J9 VM (2.9 JRE 1.8.0 AIX ppc64-64-Bit Compressed References 20190124_408237 (JIT enabled, AOT enabled)
OpenJ9   - 9c77d86
OMR      - dad8ba7
IBM      - e2996d1):

"main" - Thread t@1
   java.lang.Thread.State: RUNNABLE
 at com.ibm.security.bootstrap.SHA2.implCompress(SHA2.java:278)
 at com.ibm.security.bootstrap.DigestBase.engineUpdate(DigestBase.java:157)
 at java.security.MessageDigest$Delegate.engineUpdate(MessageDigest.java:595)
 at java.security.MessageDigest.update(MessageDigest.java:336)
 at sun.security.util.ManifestEntryVerifier.update(ManifestEntryVerifier.java:185)
 at java.util.jar.JarVerifier.update(JarVerifier.java:238)
 at java.util.jar.JarInputStream.read(JarInputStream.java:223)
 at java.io.FilterInputStream.read(FilterInputStream.java:118)
 at JavaJVMJarInputStream.jarEntryReadInsertDB(JavaJVMJarInputStream.java:111)
 at JavaJVMJarInputStream.main(JavaJVMJarInputStream.java:88)
If we list all Security Providers by:

$ > $ORACLE_HOME/jdk/bin/java -cp $ORACLE_HOME/jdbc/lib/ojdbc8.jar:. \
                                  JavaJVMJarInputStream \
                                 "jdbc:oracle:thin:k/s@testdb194:1522:testdb194" true info
                                 
 ===============0. Provider: IBMJSSE2 version 1.8
 ===============1. Provider: IBMJCE version 1.8
           ----- 23. Element:MessageDigest.SHA2
 ===============2. Provider: IBMJGSSProvider version 8.0
 ===============3. Provider: IBMCertPath version 1.8
 ===============4. Provider: IBMSASL version 1.8
 ===============5. Provider: IBMXMLCRYPTO version 8.0
 ===============6. Provider: IBMXMLEnc version 8.0
 ===============7. Provider: IBMSPNEGO version 8.0
 ===============8. Provider: SUN version 1.8
           ----- 1. Element:Provider.id info
           ----- 2. Element:Provider.id className
           ----- 3. Element:Policy.JavaPolicy
           ----- 4. Element:Provider.id version
           ----- 5. Element:Provider.id name
we can see "MessageDigest.SHA2" is only provided by "IBMJCE version 1.8". There is one single "SUN version 1.8" Provider, which contains 5 Elements.


7. Related Work


Here two Blogs about Java JIT native compiler:
     Remove Stale Native Code Files on AIX
     What the heck are the /dev/shm/JOXSHM_EXT_x files on Linux?

One Blog about both PLSQL and JAVA native compiler code:
     Native Code Files: Ora-7445 [Ioc_pin_shared_executable_object()] Reproducing

One Blog about Linux:
     joez: Failed loading machine code: Unable to allocate code space
    On docker, Ubuntu and Oracle RDBMS

One Blog about SHA2 provider performance in Linux:
    SHA2 calculation extremely slow under unknown conditions

In Oracle, there is one Java error message (ORA-10880). We also tried this event, but no lines recorded in the trace file.
  ORA-10880: trace Java VM execution

  alter session set events '10880 trace name context forever, level 4294967295'; 
    (where 4294967295 (2^32-1) seems the maximum tracing level with most details)
By the way, besides JIT, IBM JVM (platform-specific) is advanced by its Ahead-Of-Time (AOT).


8. OracleJVM Test Code



create or replace and compile java source named "OracleJVMJarInputStream" as 
import java.io.FileInputStream; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.util.Date;
import java.util.Arrays; 
import java.util.jar.JarInputStream; 
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import java.util.jar.Attributes;
import java.util.zip.ZipEntry; 
import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.CallableStatement;
import java.sql.Blob;
import oracle.jdbc.OracleDriver;
import oracle.jdbc.OracleTypes;
import oracle.jdbc.OraclePreparedStatement;
import oracle.sql.BLOB;
import java.security.Provider;
import java.security.Security;
import java.util.Enumeration;

class OracleJVMJarInputStream
{   
    static String INSERT_DB = "begin :1 := insert_blob(:2, :3); end;";
    
    public static void run(String verifyStr, String infoStr) throws Exception {   
        boolean verify    = verifyStr.equalsIgnoreCase("true")? true:false;
        boolean printInfo = infoStr.equalsIgnoreCase("info")? true:false;
        FileInputStream fis = new FileInputStream("/tmp/testJar.jar"); 
        JarInputStream jis  = new JarInputStream(fis, verify);  // default verify is true
        
        if (printInfo) {
            printManifest(jis);
            printSecurityProviders();
        }    
        
        Connection conn;       
        conn = new OracleDriver().defaultConnection();
        
        long start, finish, elapsed;
        int  seq = 1;
        JarEntry je;
        String entryName;
        start = System.currentTimeMillis();
        
        System.out.println("\n********* getNextJarEntry *********");
        try {
             while ((je = jis.getNextJarEntry()) != null) { 
               finish = System.currentTimeMillis();
               elapsed = finish - start;
               entryName = je.getName();
               System.out.println("------ NextJarEntry: " + seq + ", Name: " + entryName + " ------"); 
               System.out.println("         getNextEntry ElapsedMills: " + elapsed + ", at: " + finish); 
               seq++;
               start = System.currentTimeMillis(); 
               jarEntryReadInsertDB(conn, entryName, jis);
               finish = System.currentTimeMillis();
               elapsed = finish - start;
               System.out.println("         readContent ElapsedMills: " + elapsed + ", at: " + finish); 
               start = System.currentTimeMillis();
             }        
             jis.close(); 
             conn.close();
        } catch (Exception e) {
             e.printStackTrace();
        }        
    }
    
    static void jarEntryReadInsertDB(Connection conn, String jarEntryName, JarInputStream jis) {
        BLOB blob = null;
        byte[] buf;
        OutputStream blobStream;
        int bytesRead;     
        try {
             blob = BLOB.createTemporary(conn, false, BLOB.DURATION_SESSION);
             buf = new byte[blob.getChunkSize()];
             blobStream = blob.getBinaryOutputStream();
             while (jis.available() > 0) {
                 bytesRead = jis.read(buf);
                 blobStream.write(buf, 0, bytesRead);
             }          
             CallableStatement cStmt = conn.prepareCall(INSERT_DB);
             cStmt.registerOutParameter(1, OracleTypes.INTEGER);
             cStmt.setString(2, jarEntryName);
             cStmt.setBlob(3, blob);
             cStmt.execute();
             int insRows = cStmt.getInt(1);
             System.out.println("         Insert DB " + insRows + " row, blob size " + blob.length() + " at " + new Date());
             blob.freeTemporary();
             cStmt.close();
             
             // OraclePreparedStatement pstmt = (OraclePreparedStatement) conn.prepareStatement("insert into test_blob_tab values(?, ?)");
             // pstmt.setString(1, jarEntryName);
             // pstmt.setBLOB(2, blob);
             // pstmt.execute();
             // pstmt.close();
        } catch (Exception e) {
             e.printStackTrace();
        }
    } 

    static void printManifest(JarInputStream jis) {    
        Manifest manifest = jis.getManifest();
        
        if (manifest != null) {
            System.out.println("********* Print Manifest *********");
            System.out.println("Manifest="  +  manifest.toString());
            Attributes mainAttributes = manifest.getMainAttributes();
            System.out.println("Manifest Manifest-Version="  +  mainAttributes.getValue("Manifest-Version"));
            System.out.println("Manifest Created-By="        +  mainAttributes.getValue("Created-By"));
        } else {
            System.out.println("********* No Manifest *********");
        }     
    }
    
    static void printSecurityProviders() throws Exception {
        Provider p[] = Security.getProviders();
        for (int i = 0; i < p.length; i++) {
            System.out.println(" ===============" + i + ". Provider: " + p[i]);
            int j = 1;
            for (Enumeration e = p[i].keys(); e.hasMoreElements();) {
                 System.out.println("\t   ----- " + j + ". Element:" + e.nextElement());
                 j++;
            }
        }
    }       
}
/

create or replace procedure OracleJVMJarInputStream(p_verify varchar2, p_info varchar2) as language java
name 'OracleJVMJarInputStream.run(java.lang.String, java.lang.String)';
/

--- Test Steps ---
Sqlplus > set serveroutput on size 50000
Sqlplus > exec dbms_java.set_output(50000); 

--- Verify true ---
Sqlplus > exec OracleJVMJarInputStream('true', 'no');

--- Verify false ---
Sqlplus > exec OracleJVMJarInputStream('false', 'no');

--- Verify true, print manifest and security providers ---
Sqlplus > exec OracleJVMJarInputStream('true', 'info');


9. Standard Java JVM Test Code


           
import java.io.FileInputStream; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.util.Date;
import java.util.Arrays; 
import java.util.Enumeration;
import java.util.jar.JarInputStream; 
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import java.util.jar.Attributes;
import java.util.zip.ZipEntry; 
import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.CallableStatement;
import java.sql.Blob;
import java.security.Provider;
import java.security.Security;
import oracle.jdbc.OracleDriver;
import oracle.jdbc.OracleTypes;
import oracle.jdbc.OraclePreparedStatement;
import oracle.sql.BLOB;

/* --------------------- DB Setup ---------------------
   create table test_blob_tab(jar_name varchar2(100), jar_entry blob, sts timestamp with time zone);
   
   create or replace function insert_blob(p_jar_name varchar2, p_jar_entry blob) return number as
     l_rowcount integer;
   begin
     insert into test_blob_tab values (p_jar_name, p_jar_entry, systimestamp);
     l_rowcount := sql%rowcount;
     commit;
     return l_rowcount;
   end;
   /
   
   --------------------- Java Compile and Run (JarInputStream verify true or false)---------------------
   $ORACLE_HOME/jdk/bin/javac -cp $ORACLE_HOME/jdbc/lib/ojdbc8.jar JavaJVMJarInputStream.java
   $ORACLE_HOME/jdk/bin/java -cp $ORACLE_HOME/jdbc/lib/ojdbc8.jar:. JavaJVMJarInputStream \
                 "jdbc:oracle:thin:k/s@testdb194:1522:testdb194" true  no 
   $ORACLE_HOME/jdk/bin/java -cp $ORACLE_HOME/jdbc/lib/ojdbc8.jar:. JavaJVMJarInputStream \
                 "jdbc:oracle:thin:k/s@testdb194:1522:testdb194" false no
*/

class JavaJVMJarInputStream
{   
    static String INSERT_DB = "begin :1 := insert_blob(:2, :3); end;";
   
    public static void main(String[] args) throws Exception 
    {   
        String jdbcURL    = args[0];
        boolean verify    = args[1].equalsIgnoreCase("true")? true:false;
        boolean printInfo = args[2].equalsIgnoreCase("info")? true:false;
        FileInputStream fis = new FileInputStream("/tmp/testJar.jar"); 
        JarInputStream jis  = new JarInputStream(fis, verify);  // default verify is true
        
        if (printInfo) {
            printManifest(jis);
            printSecurityProviders();
        }
        
        Connection conn = null;
        try {
            Class.forName("oracle.jdbc.driver.OracleDriver");
            conn = DriverManager.getConnection(jdbcURL);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }                     
  
        long start, finish, elapsed;
        int  seq = 1;
        JarEntry je;
        String entryName;
        start = System.currentTimeMillis();
        
        System.out.println("\n********* getNextJarEntry *********");
        try {
             while ((je = jis.getNextJarEntry()) != null) { 
               finish = System.currentTimeMillis();
               elapsed = finish - start;
               entryName = je.getName();
               System.out.println("------ NextJarEntry: " + seq + ", Name: " + entryName + " ------"); 
               System.out.println("         getNextEntry ElapsedMills: " + elapsed + ", at: " + finish); 
               seq++;
               start = System.currentTimeMillis(); 
               jarEntryReadInsertDB(conn, entryName, jis);
               finish = System.currentTimeMillis();
               elapsed = finish - start;
               System.out.println("         readContent ElapsedMills: " + elapsed + ", at: " + finish); 
               start = System.currentTimeMillis();
             }        
             jis.close(); 
             conn.close();
        } catch (Exception e) {
             e.printStackTrace();
        }        
    }
    
    static void jarEntryReadInsertDB(Connection conn, String jarEntryName, JarInputStream jis) {
        BLOB blob = null;
        byte[] buf;
        OutputStream blobStream;
        int bytesRead;     
        try {
             blob = BLOB.createTemporary(conn, false, BLOB.DURATION_SESSION);
             buf = new byte[blob.getChunkSize()];
             blobStream = blob.getBinaryOutputStream();
             while (jis.available() > 0) {
                 bytesRead = jis.read(buf);
                 blobStream.write(buf, 0, bytesRead);
             }         
             CallableStatement cStmt = conn.prepareCall(INSERT_DB);
             cStmt.registerOutParameter(1, OracleTypes.INTEGER);
             cStmt.setString(2, jarEntryName);
             cStmt.setBlob(3, blob);
             cStmt.execute();
             int insRows = cStmt.getInt(1);
             System.out.println("         Insert DB " + insRows + " row, blob size " + blob.length() + " at " + new Date());
             blob.freeTemporary();
             cStmt.close();
             
             // OraclePreparedStatement pstmt = (OraclePreparedStatement) conn.prepareStatement("insert into test_blob_tab values(?, ?)");
             // pstmt.setString(1, jarEntryName);
             // pstmt.setBLOB(2, blob);
             // pstmt.execute();
             // pstmt.close();
        } catch (Exception e) {
             e.printStackTrace();
        }
    } 

    static void printManifest(JarInputStream jis) {    
        Manifest manifest = jis.getManifest();
        
        if (manifest != null) {
            System.out.println("********* Print Manifest *********");
            System.out.println("Manifest="  +  manifest.toString());
            Attributes mainAttributes = manifest.getMainAttributes();
            System.out.println("Manifest Manifest-Version="  +  mainAttributes.getValue("Manifest-Version"));
            System.out.println("Manifest Created-By="        +  mainAttributes.getValue("Created-By"));
        } else {
            System.out.println("********* No Manifest *********");
        }     
    }
    
    static void printSecurityProviders() throws Exception {
        Provider p[] = Security.getProviders();
        for (int i = 0; i < p.length; i++) {
            System.out.println(" ===============" + i + ". Provider: " + p[i]);
            int j = 1;
            for (Enumeration e = p[i].keys(); e.hasMoreElements();) {
                 System.out.println("\t   ----- " + j + ". Element:" + e.nextElement());
                 j++;
            }
        }
    }       
}