6
0
mirror of https://github.com/FirebirdSQL/firebird-qa.git synced 2025-01-22 13:33:07 +01:00
firebird-qa/tests/bugs/gh_7917_test-obj-in-use-on-drop-db.py

420 lines
17 KiB
Python

#coding:utf-8
"""
ID: issue-7917
ISSUE: https://github.com/FirebirdSQL/firebird/issues/7917
TITLE: Hang in a case of error when sweep thread is attaching to database
DESCRIPTION:
Test uses preliminary created alias with KeyHolderPlugin that points to special configuration with name 'KH2'.
This configuration makes crypt plugin accept key ONLY from client app (in contrary to some other encryption-related tests).
DB-level triggers (ON CONNECT, ON DISCONNECT) are created for logging appropriate events (see table 'att_log').
Then we run special SQL that uses autonomous transactions and ES/EDS.
This script will hang because of update-conflict after some number of transactions (see TX_NUMBER_BEFORE_HANG).
We run this script asynchronously, then reduce sweep interval to value SWP_INTERVAL_TO_CHECK that is less than TX_NUMBER_BEFORE_HANG,
change DB state to full shutdown and return it online.
At this point any connection to DB will fire AUTO SWEEP (normally this can be seen in firebird.log as 'Sweep is started by SWEEPER').
We run ISQL that queries 'att_log' table and causes AUTO SWEEP. That ISQL has to normally detach from DB and we must see its results.
NOTES:
[28.12.2023] pzotov
1. To make crypt plugin accept key only from client app, $FB_HOME/plugins.conf must contain following lines:
==================
Plugin = KH2 {
Module = $(dir_plugins)/fbSampleKeyHolder
RegisterName = fbSampleKeyHolder
Config = KH2
}
Config = KH2 {
Auto = false
}
==================
QA-scenario (.bat and .sh) must add in advance content of $QA_ROOT/files/qa-plugins-supplement.conf to $FB_HOME/plugins.conf.
2. Demo-plugin (fbSampleKeyHolder) can transfer key over network only for default key which has no-name.
Because of this, command 'alter database encrypt with <plugin>' has no '... key <key>' tail.
See letter from Alex, 15.12.2023 16:16
3. In case of regression caused by that bug, we have to be ready that FB will hang on this test!
Great thanks to Alex for suggestions (discussion started 13.12.2023 13:18).
Confirmed bug on 6.0.0.173.
Checked on 6.0.0.175, 5.0.0.1305, 4.0.5.3049 - but currenlyt only SuperServer works fine.
Classic has the same problem. Sent report to Alex, 28.12.2023 13:10.
On 3.0.12.33726 error raises:
unsuccessful metadata update / -ALTER DATABASE failed / -Missing correct crypt key / -Plugin fbSampleDbCrypt: / -Crypt key not set
This problem not [yet] investogated.
"""
import os
import time
import datetime as py_dt
import subprocess
import locale
import re
from pathlib import Path
import pytest
from firebird.qa import *
from firebird.driver import DatabaseError
REQUIRED_ALIAS = 'tmp_gh_7917_alias'
###########################
### S E T T I N G S ###
###########################
# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings
# from act.files_dir/'test_config.ini':
enc_settings = QA_GLOBALS['encryption']
# ACHTUNG: this must be carefully tuned on every new host:
#
MAX_WAITING_ENCR_FINISH = int(enc_settings['MAX_WAIT_FOR_ENCR_FINISH_WIN' if os.name == 'nt' else 'MAX_WAIT_FOR_ENCR_FINISH_NIX'])
assert MAX_WAITING_ENCR_FINISH > 0
ENCRYPTION_PLUGIN = enc_settings['encryption_plugin'] # fbSampleDbCrypt
ENCRYPTION_KEY = enc_settings['encryption_key'] # Red
SWP_INTERVAL_TO_CHECK = 100
TX_NUMBER_BEFORE_HANG = SWP_INTERVAL_TO_CHECK + 10
MAX_WAIT_FOR_ISQL_TERMINATE=11
db = db_factory(filename = '#' + REQUIRED_ALIAS, do_not_drop = True)
act = python_act('db', substitutions = [('^((?!(ISQL|Attributes)).)*$', ''), ('[ \t]+', ' '), ('TCPv(4|6)$', 'TCP')])
#act = python_act('db')
tmp_sql_file = temp_file('tmp_7917.sql')
tmp_log_file = temp_file('tmp_7917.log')
#-----------------------------------------------------------------------
def run_encr_decr(act: Action, mode, max_wait_encr_thread_finish, capsys):
if mode == 'encrypt':
# See letter from Alex, 15.12.2023 16:16 demo-plugin can not transfer named key over network.
# Because of that, we have to use following command WITHOUT adding 'key "{ENCRYPTION_KEY}"':
# ::: NB ::: One need to be sure that $FB_HOME/plugins.conf contains following lines:
# Plugin = KH2 {
# Module = $(dir_plugins)/fbSampleKeyHolder
# RegisterName = fbSampleKeyHolder
# Config = KH2
# }
# Config = KH2 {
# Auto = false
# }
# Otherwise error will raise:
# unsuccessful metadata update
# -ALTER DATABASE failed
# -Missing database encryption key for your attachment
# -Plugin fbSampleDbCrypt:
# -Crypt key not set
#
alter_db_sttm = f'alter database encrypt with "{ENCRYPTION_PLUGIN}"'
wait_for_state = 'Database encrypted'
elif mode == 'decrypt':
alter_db_sttm = 'alter database decrypt'
wait_for_state = 'Database not encrypted'
e_thread_started = False
e_thread_finished = False
with act.db.connect() as con:
t1=py_dt.datetime.now()
d1 = t1-t1
try:
con.execute_immediate(alter_db_sttm)
con.commit()
e_thread_started = True
except DatabaseError as e:
print( e.__str__() )
while e_thread_started:
t2=py_dt.datetime.now()
d1=t2-t1
if d1.seconds*1000 + d1.microseconds//1000 > max_wait_encr_thread_finish:
print(f'TIMEOUT EXPIRATION. Mode="{mode}" took {d1.seconds*1000 + d1.microseconds//1000} ms which exceeds limit = {max_wait_encr_thread_finish} ms.')
break
# Possible output:
# Database [not] encrypted
# Database encrypted, crypt thread not complete
act.isql(switches=['-q'], input = 'show database;', combine_output = True)
if wait_for_state in act.stdout:
if 'not complete' in act.stdout:
pass
else:
e_thread_finished = True
break
act.reset()
act.expected_stdout = ''
act.stdout = capsys.readouterr().out
assert act.clean_stdout == act.clean_expected_stdout
act.reset()
assert e_thread_finished
#-----------------------------------------------------------------------
@pytest.mark.es_eds
@pytest.mark.encryption
@pytest.mark.version('>=4.0.5')
@pytest.mark.platform('Windows')
def test_1(act: Action, tmp_sql_file: Path, tmp_log_file: Path, capsys):
'''
with act.db.connect() as con:
if act.vars['server-arch'] == 'SuperServer':
pass
else:
pytest.skip("Currently fixed only for SuperServer. Temporary SKIPPED.")
'''
# Scan line-by-line through databases.conf, find line starting with REQUIRED_ALIAS and extract name of file that
# must be created in the $(dir_sampleDb)/qa/ folder. This name will be used further as target database (tmp_fdb).
# NOTE: we have to SKIP lines which are commented out, i.e. if they starts with '#':
p_required_alias_ptn = re.compile( '^(?!#)((^|\\s+)' + REQUIRED_ALIAS + ')\\s*=\\s*\\$\\(dir_sampleDb\\)/qa/', re.IGNORECASE )
fname_in_dbconf = None
with open(act.home_dir/'databases.conf', 'r') as f:
for line in f:
if p_required_alias_ptn.search(line):
# If databases.conf contains line like this:
# tmp_7598_alias = $(dir_sampleDb)/qa/tmp_gh_7598.fdb
# - then we extract filename: 'tmp_gh_7598.fdb' (see below):
fname_in_dbconf = Path(line.split('=')[1].strip()).name
break
# if 'fname_in_dbconf' remains undefined here then propably REQUIRED_ALIAS not equals to specified in the databases.conf!
#
assert fname_in_dbconf
#----------------------------------------------------------------
run_encr_decr(act, 'encrypt', MAX_WAITING_ENCR_FINISH, capsys)
test_script = f"""
set bail on;
create table att_log(
att_prot varchar(15)
,who_ami varchar(31) default current_user
,att_id bigint default current_connection
,trn_id bigint default current_transaction
,evt_time time default 'now'
,evt_name varchar(20)
,swp_interval int
);
set term ^;
create procedure sp_fill_dblevel_log(a_evt_name type of column att_log.evt_name) as
declare v_swp_interval int;
declare v_protocol type of column att_log.att_prot;
begin
insert into att_log(
att_prot
,evt_name
) values (
rdb$get_context('SYSTEM', 'NETWORK_PROTOCOL')
,:a_evt_name
);
end
^
create or alter trigger trg_detach on disconnect as
begin
execute procedure sp_fill_dblevel_log('detach');
end
^
create or alter trigger trg_attach on connect as
begin
execute procedure sp_fill_dblevel_log('attach');
end
^
set term ;^
commit;
recreate table test(s varchar(36) unique);
insert into test(s) values('LOCKED_FOR_PAUSE');
commit;
set transaction read committed WAIT;
update test set s = s where s = 'LOCKED_FOR_PAUSE';
set term ^;
execute block as
declare n int = {TX_NUMBER_BEFORE_HANG};
declare v_role varchar(31);
begin
while (n > 0) do
in autonomous transaction do
insert into test(s) values( rpad('', 36, uuid_to_char(gen_uuid()) ) )
returning :n-1 into n;
v_role = left(replace( uuid_to_char(gen_uuid()), '-', ''), 31);
begin
execute statement ('update /* ES/EDS */ test set s = s where s = ?') ('LOCKED_FOR_PAUSE')
on external
'localhost:' || rdb$get_context('SYSTEM', 'DB_NAME')
as user 'SYSDBA' password 'masterkey' role v_role
with autonomous transaction;
when any do
begin
end
end
end
^
set term ;^
set heading off;
select '-- shutdown me now --' from rdb$database;
"""
tmp_sql_file.write_text(test_script)
#----------------------------------------------------------------
# Reduce sweep interval to small value (that must be less than SQL_HANG_AFTER_TX_CNT):
#
act.gfix(switches=['-h', f'{SWP_INTERVAL_TO_CHECK}', act.db.dsn], combine_output = True, io_enc = locale.getpreferredencoding())
with open(tmp_log_file, 'w') as f:
# Launch ISQL which will hang because update conflict.
# This ISQl will be 'self-terminated' further because we will change DB state to full shutdown:
#
p_handed_isql = subprocess.Popen([act.vars['isql'], '-nod', '-i', str(tmp_sql_file),
'-user', act.db.user,
'-password', act.db.password, act.db.dsn],
stdout = f,
stderr = subprocess.STDOUT)
# Let ISQL time to establish connection and fall in hanging state:
time.sleep(3)
try:
act.gfix(switches=['-shut', 'full', '-force', '0', act.db.dsn], combine_output = True, io_enc = locale.getpreferredencoding())
finally:
p_handed_isql.terminate()
p_handed_isql.wait(MAX_WAIT_FOR_ISQL_TERMINATE)
if p_handed_isql.poll() is None:
print(f'Hanged ISQL process WAS NOT terminated in {MAX_WAIT_FOR_ISQL_TERMINATE} second(s).!')
else:
print(f'Hanged ISQL process terminated with retcode = {p_handed_isql.poll()}')
# Result: log of hanged ISQL must contain now:
# Statement failed, SQLSTATE = 08003
# connection shutdown
# -Database is shutdown.
act.gfix(switches=['-online', act.db.dsn], combine_output = True, io_enc = locale.getpreferredencoding())
assert act.clean_stdout == ''
act.reset()
# Must show: Attributes encrypted, plugin {ENCRYPTION_PLUGIN} - without 'shutdown'!
act.gstat(switches=['-h'])
print(act.stdout)
#act.stdout = capsys.readouterr().out
#assert act.clean_stdout == act.clean_expected_stdout
#act.reset()
#----------------------------------------------------------------
TEST_QUERY = 'select att_prot,who_ami,evt_name from att_log order by trn_id'
final_sql = f"""
set count on;
set list on;
set echo on;
{TEST_QUERY};
commit;
connect '{act.db.dsn}';
drop database;
quit;
"""
tmp_sql_file.write_text(final_sql)
with open(tmp_log_file, 'w') as f:
# Explained by Alex, letter 13-dec-2023 13:18.
# Following ISQL will create attach that provokes AUTO SWEEP (because Next - OST now greater than SWP_INTERVAL_TO_CHECK).
# Problem raised when other attachments were prohibited to use encryption key (and this is default behaviour).
# Before fix, SWEEEP was not allowed to use key from this ISQL-attachment.
# Following message was added in firebird.log: "Automatic sweep error /Missing database encryption key for your attachment"
# But despite problem with establishing connection by SWEEP, its thread already created appropriate lock at that point.
# As result, engine remained in wrong state after this: existied attachments could not be closed.
# Also, FB process could not be normally stopped.
MAX_WAIT_AUTO_SWEEP_FINISH = 3
p_chk_sql = subprocess.Popen( [ act.vars['isql'],
'-nod', '-i', str(tmp_sql_file),
'-user', act.db.user,
'-password', act.db.password,
act.db.dsn
],
stdout = f,
stderr = subprocess.STDOUT,
)
# If the process does not terminate after timeout seconds, raise a TimeoutExpired exception.
# It is safe to catch this exception and retry the wait.
try:
p_chk_sql.wait(timeout = MAX_WAIT_AUTO_SWEEP_FINISH)
except subprocess.TimeoutExpired as e:
print(f'Could not obtain result for {MAX_WAIT_AUTO_SWEEP_FINISH} seconds:')
print(e.__str__())
p_chk_sql.terminate()
p_chk_sql.wait(MAX_WAIT_FOR_ISQL_TERMINATE)
# Check if child process has terminated. Set and return returncode attribute. Otherwise, returns None.
if p_chk_sql.poll() is None:
print(f'Final ISQL process WAS NOT terminated in {MAX_WAIT_FOR_ISQL_TERMINATE} second(s).!')
else:
print(f'Final ISQL process terminated')
#print(f'Final ISQL process terminated with retcode = {p_chk_sql.poll()}')
# do NOT put here this:
#run_encr_decr(act, 'decrypt', MAX_WAITING_ENCR_FINISH, capsys)
# - otherwise pytest will not return control
with open(tmp_log_file, 'r') as f:
for line in f:
if line.strip():
print('final ISQL log:',line)
act.expected_stdout = f"""
Hanged ISQL process terminated with retcode = 1
Attributes encrypted, plugin {ENCRYPTION_PLUGIN}
Final ISQL process terminated
final ISQL log: {TEST_QUERY};
final ISQL log: ATT_PROT TCP
final ISQL log: WHO_AMI {act.db.user.upper()}
final ISQL log: EVT_NAME attach
final ISQL log: ATT_PROT TCP
final ISQL log: WHO_AMI {act.db.user.upper()}
final ISQL log: EVT_NAME detach
final ISQL log: Records affected: 2
final ISQL log: commit;
final ISQL log: drop database;
final ISQL log: quit;
"""
act.stdout = capsys.readouterr().out
assert act.clean_stdout == act.clean_expected_stdout
act.reset()
# NB! We have to decrypt database now. Otherwise teardown will fail with:
# firebird.driver.types.DatabaseError: Missing database encryption key for your attachment
# -Plugin fbSampleDbCrypt:
# -Crypt key not set
#run_encr_decr(act, 'decrypt', MAX_WAITING_ENCR_FINISH, capsys)