2016-03-02 21:00:30 +00:00
import distutils . version
2019-01-09 22:59:03 +00:00
from . import HydrusConstants as HC
from . import HydrusData
from . import HydrusExceptions
from . import HydrusGlobals as HG
from . import HydrusPaths
2019-01-30 22:14:54 +00:00
from . import HydrusText
2015-04-22 22:57:25 +00:00
import os
2019-01-09 22:59:03 +00:00
import queue
2015-04-22 22:57:25 +00:00
import sqlite3
import traceback
import time
2016-04-20 20:42:21 +00:00
CONNECTION_REFRESH_TIME = 60 * 30
2016-04-14 01:54:29 +00:00
def CanVacuum ( db_path , stop_time = None ) :
2016-04-20 20:42:21 +00:00
try :
db = sqlite3 . connect ( db_path , isolation_level = None , detect_types = sqlite3 . PARSE_DECLTYPES )
2016-04-14 01:54:29 +00:00
2016-04-20 20:42:21 +00:00
c = db . cursor ( )
2016-04-14 01:54:29 +00:00
2016-04-20 20:42:21 +00:00
( page_size , ) = c . execute ( ' PRAGMA page_size; ' ) . fetchone ( )
( page_count , ) = c . execute ( ' PRAGMA page_count; ' ) . fetchone ( )
( freelist_count , ) = c . execute ( ' PRAGMA freelist_count; ' ) . fetchone ( )
2016-04-14 01:54:29 +00:00
2016-04-20 20:42:21 +00:00
db_size = ( page_count - freelist_count ) * page_size
2016-04-14 01:54:29 +00:00
2016-04-20 20:42:21 +00:00
if stop_time is not None :
2016-04-14 01:54:29 +00:00
2016-05-11 18:16:39 +00:00
approx_vacuum_speed_mb_per_s = 1048576 * 1
2016-04-14 01:54:29 +00:00
2019-01-09 22:59:03 +00:00
approx_vacuum_duration = db_size / / approx_vacuum_speed_mb_per_s
2016-04-20 20:42:21 +00:00
time_i_will_have_to_start = stop_time - approx_vacuum_duration
2016-04-14 01:54:29 +00:00
2016-04-20 20:42:21 +00:00
if HydrusData . TimeHasPassed ( time_i_will_have_to_start ) :
return False
2016-04-14 01:54:29 +00:00
2016-04-20 20:42:21 +00:00
( db_dir , db_filename ) = os . path . split ( db_path )
2016-04-14 01:54:29 +00:00
2017-03-08 23:23:12 +00:00
( has_space , reason ) = HydrusPaths . HasSpaceForDBTransaction ( db_dir , db_size )
2016-04-14 01:54:29 +00:00
2017-03-08 23:23:12 +00:00
return has_space
2016-04-20 20:42:21 +00:00
except Exception as e :
HydrusData . Print ( ' Could not determine whether to vacuum or not: ' )
HydrusData . PrintException ( e )
return False
2016-04-14 01:54:29 +00:00
2018-11-28 22:31:04 +00:00
def ReadLargeIdQueryInSeparateChunks ( cursor , select_statement , chunk_size ) :
2019-02-13 22:26:43 +00:00
table_name = ' tempbigread ' + os . urandom ( 32 ) . hex ( )
2018-11-28 22:31:04 +00:00
2019-02-13 22:26:43 +00:00
cursor . execute ( ' CREATE TEMPORARY TABLE ' + table_name + ' ( job_id INTEGER PRIMARY KEY AUTOINCREMENT, temp_id INTEGER ); ' )
2018-11-28 22:31:04 +00:00
cursor . execute ( ' INSERT INTO ' + table_name + ' ( temp_id ) ' + select_statement ) # given statement should end in semicolon, so we are good
2019-02-13 22:26:43 +00:00
num_to_do = cursor . rowcount
2018-11-28 22:31:04 +00:00
2019-02-13 22:26:43 +00:00
if num_to_do is None or num_to_do == - 1 :
2018-11-28 22:31:04 +00:00
2019-02-13 22:26:43 +00:00
num_to_do = 0
2018-11-28 22:31:04 +00:00
2019-02-13 22:26:43 +00:00
i = 0
while i < num_to_do :
chunk = [ temp_id for ( temp_id , ) in cursor . execute ( ' SELECT temp_id FROM ' + table_name + ' WHERE job_id BETWEEN ? AND ?; ' , ( i , i + chunk_size - 1 ) ) ]
yield chunk
i + = chunk_size
2018-11-28 22:31:04 +00:00
cursor . execute ( ' DROP TABLE ' + table_name + ' ; ' )
2016-04-14 01:54:29 +00:00
def VacuumDB ( db_path ) :
db = sqlite3 . connect ( db_path , isolation_level = None , detect_types = sqlite3 . PARSE_DECLTYPES )
c = db . cursor ( )
( previous_journal_mode , ) = c . execute ( ' PRAGMA journal_mode; ' ) . fetchone ( )
fast_big_transaction_wal = not distutils . version . LooseVersion ( sqlite3 . sqlite_version ) < distutils . version . LooseVersion ( ' 3.11.0 ' )
if previous_journal_mode == ' wal ' and not fast_big_transaction_wal :
c . execute ( ' PRAGMA journal_mode = TRUNCATE; ' )
if HC . PLATFORM_WINDOWS :
ideal_page_size = 4096
else :
ideal_page_size = 1024
( page_size , ) = c . execute ( ' PRAGMA page_size; ' ) . fetchone ( )
if page_size != ideal_page_size :
c . execute ( ' PRAGMA journal_mode = TRUNCATE; ' )
c . execute ( ' PRAGMA page_size = ' + str ( ideal_page_size ) + ' ; ' )
2017-07-12 20:03:45 +00:00
c . execute ( ' PRAGMA auto_vacuum = 0; ' ) # none
2016-04-14 01:54:29 +00:00
c . execute ( ' VACUUM; ' )
if previous_journal_mode == ' wal ' :
c . execute ( ' PRAGMA journal_mode = WAL; ' )
2015-04-22 22:57:25 +00:00
class HydrusDB ( object ) :
READ_WRITE_ACTIONS = [ ]
2016-03-09 19:37:14 +00:00
UPDATE_WAIT = 2
2015-04-22 22:57:25 +00:00
2017-11-01 20:37:39 +00:00
TRANSACTION_COMMIT_TIME = 10
2019-03-20 21:22:10 +00:00
def __init__ ( self , controller , db_dir , db_name ) :
2015-08-26 21:18:39 +00:00
2018-08-15 20:40:30 +00:00
if HydrusPaths . GetFreeSpace ( db_dir ) < 500 * 1048576 :
raise Exception ( ' Sorry, it looks like the db partition has less than 500MB, please free up some space. ' )
2015-08-26 21:18:39 +00:00
self . _controller = controller
2016-04-06 19:52:45 +00:00
self . _db_dir = db_dir
self . _db_name = db_name
2015-04-22 22:57:25 +00:00
2017-07-05 21:09:28 +00:00
self . _transaction_started = 0
2017-03-29 19:39:34 +00:00
self . _in_transaction = False
2017-07-12 20:03:45 +00:00
self . _transaction_contains_writes = False
2017-03-29 19:39:34 +00:00
2016-04-20 20:42:21 +00:00
self . _connection_timestamp = 0
2016-04-06 19:52:45 +00:00
main_db_filename = db_name
if not main_db_filename . endswith ( ' .db ' ) :
main_db_filename + = ' .db '
self . _db_filenames = { }
self . _db_filenames [ ' main ' ] = main_db_filename
2016-04-20 20:42:21 +00:00
self . _InitExternalDatabases ( )
2016-04-06 19:52:45 +00:00
if distutils . version . LooseVersion ( sqlite3 . sqlite_version ) < distutils . version . LooseVersion ( ' 3.11.0 ' ) :
2016-03-02 21:00:30 +00:00
self . _fast_big_transaction_wal = False
else :
self . _fast_big_transaction_wal = True
2016-08-31 19:55:14 +00:00
self . _is_first_start = False
self . _is_db_updated = False
2015-04-22 22:57:25 +00:00
self . _local_shutdown = False
self . _loop_finished = False
2016-03-16 22:19:14 +00:00
self . _ready_to_serve_requests = False
self . _could_not_initialise = False
2015-04-22 22:57:25 +00:00
2019-01-09 22:59:03 +00:00
self . _jobs = queue . Queue ( )
2015-04-22 22:57:25 +00:00
self . _pubsubs = [ ]
self . _currently_doing_job = False
2017-05-24 20:28:24 +00:00
self . _current_status = ' '
self . _current_job_name = ' '
2015-04-22 22:57:25 +00:00
2016-02-17 22:06:47 +00:00
self . _db = None
self . _c = None
2016-04-06 19:52:45 +00:00
if os . path . exists ( os . path . join ( self . _db_dir , self . _db_filenames [ ' main ' ] ) ) :
2015-04-22 22:57:25 +00:00
# open and close to clean up in case last session didn't close well
self . _InitDB ( )
self . _CloseDBCursor ( )
self . _InitDB ( )
2018-05-16 20:09:50 +00:00
self . _RepairDB ( )
2015-04-22 22:57:25 +00:00
( version , ) = self . _c . execute ( ' SELECT version FROM version; ' ) . fetchone ( )
2018-11-28 22:31:04 +00:00
if version > HC . SOFTWARE_VERSION :
2017-03-08 23:23:12 +00:00
2018-11-28 22:31:04 +00:00
self . _ReportOverupdatedDB ( version )
2017-03-08 23:23:12 +00:00
2019-02-13 22:26:43 +00:00
if version < ( HC . SOFTWARE_VERSION - 15 ) :
self . _ReportUnderupdatedDB ( version )
2018-11-28 22:31:04 +00:00
if version < HC . SOFTWARE_VERSION - 50 :
2017-03-08 23:23:12 +00:00
2018-11-28 22:31:04 +00:00
raise Exception ( ' Your current database version of hydrus ' + str ( version ) + ' is too old for this software version ' + str ( HC . SOFTWARE_VERSION ) + ' to update. Please try updating with version ' + str ( version + 45 ) + ' or earlier first. ' )
2017-03-08 23:23:12 +00:00
2015-04-22 22:57:25 +00:00
while version < HC . SOFTWARE_VERSION :
2016-03-09 19:37:14 +00:00
time . sleep ( self . UPDATE_WAIT )
2015-04-22 22:57:25 +00:00
2017-03-29 19:39:34 +00:00
try :
self . _BeginImmediate ( )
2015-04-22 22:57:25 +00:00
except Exception as e :
2019-01-09 22:59:03 +00:00
raise HydrusExceptions . DBAccessException ( str ( e ) )
2015-04-22 22:57:25 +00:00
try :
self . _UpdateDB ( version )
2017-03-29 19:39:34 +00:00
self . _Commit ( )
2015-04-22 22:57:25 +00:00
2016-08-31 19:55:14 +00:00
self . _is_db_updated = True
2015-04-22 22:57:25 +00:00
except :
2016-04-20 20:42:21 +00:00
e = Exception ( ' Updating the ' + self . _db_name + ' db to version ' + str ( version + 1 ) + ' caused this error: ' + os . linesep + traceback . format_exc ( ) )
try :
2017-03-29 19:39:34 +00:00
self . _Rollback ( )
2016-04-20 20:42:21 +00:00
except Exception as rollback_e :
HydrusData . Print ( ' When the update failed, attempting to rollback the database failed. ' )
HydrusData . PrintException ( rollback_e )
2015-04-22 22:57:25 +00:00
2016-04-20 20:42:21 +00:00
raise e
2015-04-22 22:57:25 +00:00
( version , ) = self . _c . execute ( ' SELECT version FROM version; ' ) . fetchone ( )
self . _CloseDBCursor ( )
2017-08-09 21:33:51 +00:00
self . _controller . CallToThreadLongRunning ( self . MainLoop )
2016-03-16 22:19:14 +00:00
while not self . _ready_to_serve_requests :
time . sleep ( 0.1 )
if self . _could_not_initialise :
raise Exception ( ' Could not initialise the db! Error written to the log! ' )
2015-04-22 22:57:25 +00:00
2016-03-30 22:56:50 +00:00
def _AttachExternalDatabases ( self ) :
2019-01-09 22:59:03 +00:00
for ( name , filename ) in list ( self . _db_filenames . items ( ) ) :
2016-04-20 20:42:21 +00:00
if name == ' main ' :
continue
db_path = os . path . join ( self . _db_dir , self . _db_filenames [ name ] )
2016-05-11 18:16:39 +00:00
self . _c . execute ( ' ATTACH ? AS ' + name + ' ; ' , ( db_path , ) )
2016-04-20 20:42:21 +00:00
2016-03-30 22:56:50 +00:00
2017-03-29 19:39:34 +00:00
def _BeginImmediate ( self ) :
2017-07-12 20:03:45 +00:00
if not self . _in_transaction :
2017-03-29 19:39:34 +00:00
self . _c . execute ( ' BEGIN IMMEDIATE; ' )
2017-07-12 20:03:45 +00:00
self . _c . execute ( ' SAVEPOINT hydrus_savepoint; ' )
2017-03-29 19:39:34 +00:00
2017-07-05 21:09:28 +00:00
self . _transaction_started = HydrusData . GetNow ( )
2017-03-29 19:39:34 +00:00
self . _in_transaction = True
2015-06-17 20:01:41 +00:00
def _CleanUpCaches ( self ) :
pass
2015-04-22 22:57:25 +00:00
def _CloseDBCursor ( self ) :
2016-02-17 22:06:47 +00:00
if self . _db is not None :
2017-03-29 19:39:34 +00:00
if self . _in_transaction :
self . _Commit ( )
2016-02-17 22:06:47 +00:00
self . _c . close ( )
self . _db . close ( )
del self . _c
del self . _db
self . _db = None
self . _c = None
2015-04-22 22:57:25 +00:00
2017-03-29 19:39:34 +00:00
def _Commit ( self ) :
if self . _in_transaction :
self . _c . execute ( ' COMMIT; ' )
self . _in_transaction = False
else :
HydrusData . Print ( ' Received a call to commit, but was not in a transaction! ' )
2015-04-22 22:57:25 +00:00
def _CreateDB ( self ) :
raise NotImplementedError ( )
2017-03-02 02:14:56 +00:00
def _CreateIndex ( self , table_name , columns , unique = False ) :
if ' . ' in table_name :
table_name_simple = table_name . split ( ' . ' ) [ 1 ]
else :
table_name_simple = table_name
index_name = table_name + ' _ ' + ' _ ' . join ( columns ) + ' _index '
if unique :
2017-03-15 20:13:04 +00:00
create_phrase = ' CREATE UNIQUE INDEX IF NOT EXISTS '
2017-03-02 02:14:56 +00:00
else :
2017-03-15 20:13:04 +00:00
create_phrase = ' CREATE INDEX IF NOT EXISTS '
2017-03-02 02:14:56 +00:00
on_phrase = ' ON ' + table_name_simple + ' ( ' + ' , ' . join ( columns ) + ' ); '
statement = create_phrase + index_name + on_phrase
self . _c . execute ( statement )
2018-02-14 21:47:18 +00:00
def _DisplayCatastrophicError ( self , text ) :
message = ' The db encountered a serious error! This is going to be written to the log as well, but here it is for a screenshot: '
message + = os . linesep * 2
message + = text
HydrusData . DebugPrint ( message )
2015-04-22 22:57:25 +00:00
def _GetRowCount ( self ) :
row_count = self . _c . rowcount
if row_count == - 1 : return 0
else : return row_count
def _InitCaches ( self ) :
2016-03-16 22:19:14 +00:00
pass
2015-04-22 22:57:25 +00:00
def _InitDB ( self ) :
2015-09-23 21:21:02 +00:00
create_db = False
2016-04-06 19:52:45 +00:00
db_path = os . path . join ( self . _db_dir , self . _db_filenames [ ' main ' ] )
if not os . path . exists ( db_path ) :
2016-01-13 22:08:19 +00:00
create_db = True
2019-02-27 23:03:30 +00:00
external_db_paths = [ os . path . join ( self . _db_dir , self . _db_filenames [ db_name ] ) for db_name in self . _db_filenames if db_name != ' main ' ]
existing_external_db_paths = [ external_db_path for external_db_path in external_db_paths if os . path . exists ( external_db_path ) ]
if len ( existing_external_db_paths ) > 0 :
message = ' Although the external files, " {} " do exist, the main database file, " {} " , does not! This makes for an invalid database, and the program will now quit. Please contact hydrus_dev if you do not know how this happened or need help recovering from hard drive failure. '
message = message . format ( ' , ' . join ( existing_external_db_paths ) , db_path )
raise HydrusExceptions . DBException ( message )
2015-04-22 22:57:25 +00:00
self . _InitDBCursor ( )
2015-09-23 21:21:02 +00:00
result = self . _c . execute ( ' SELECT 1 FROM sqlite_master WHERE type = ? AND name = ?; ' , ( ' table ' , ' version ' ) ) . fetchone ( )
if result is None :
create_db = True
2015-04-22 22:57:25 +00:00
if create_db :
2016-08-31 19:55:14 +00:00
self . _is_first_start = True
2015-04-22 22:57:25 +00:00
self . _CreateDB ( )
2017-07-12 20:03:45 +00:00
self . _Commit ( )
self . _BeginImmediate ( )
2015-04-22 22:57:25 +00:00
def _InitDBCursor ( self ) :
2016-02-17 22:06:47 +00:00
self . _CloseDBCursor ( )
2016-04-06 19:52:45 +00:00
db_path = os . path . join ( self . _db_dir , self . _db_filenames [ ' main ' ] )
2016-01-20 23:57:33 +00:00
2016-04-06 19:52:45 +00:00
db_just_created = not os . path . exists ( db_path )
self . _db = sqlite3 . connect ( db_path , isolation_level = None , detect_types = sqlite3 . PARSE_DECLTYPES )
2015-04-22 22:57:25 +00:00
2016-04-20 20:42:21 +00:00
self . _connection_timestamp = HydrusData . GetNow ( )
2015-04-22 22:57:25 +00:00
self . _c = self . _db . cursor ( )
2019-03-06 23:06:22 +00:00
if HG . no_db_temp_files :
self . _c . execute ( ' PRAGMA temp_store = 2; ' ) # use memory for temp store exclusively
2016-10-26 20:45:34 +00:00
self . _c . execute ( ' PRAGMA main.cache_size = -10000; ' )
2016-01-13 22:08:19 +00:00
2016-04-14 01:54:29 +00:00
self . _c . execute ( ' ATTACH " :memory: " AS mem; ' )
self . _AttachExternalDatabases ( )
db_names = [ name for ( index , name , path ) in self . _c . execute ( ' PRAGMA database_list; ' ) if name not in ( ' mem ' , ' temp ' ) ]
for db_name in db_names :
2016-01-20 23:57:33 +00:00
2016-10-26 20:45:34 +00:00
self . _c . execute ( ' PRAGMA ' + db_name + ' .cache_size = -10000; ' )
2016-04-20 20:42:21 +00:00
2019-03-20 21:22:10 +00:00
if HG . no_wal :
2016-01-20 23:57:33 +00:00
2016-04-14 01:54:29 +00:00
self . _c . execute ( ' PRAGMA ' + db_name + ' .journal_mode = TRUNCATE; ' )
2016-01-20 23:57:33 +00:00
2016-04-14 01:54:29 +00:00
self . _c . execute ( ' PRAGMA ' + db_name + ' .synchronous = 2; ' )
2016-02-17 22:06:47 +00:00
2016-04-14 01:54:29 +00:00
self . _c . execute ( ' SELECT * FROM ' + db_name + ' .sqlite_master; ' ) . fetchone ( )
2016-01-20 23:57:33 +00:00
2016-04-14 01:54:29 +00:00
else :
2016-01-20 23:57:33 +00:00
2016-04-14 01:54:29 +00:00
self . _c . execute ( ' PRAGMA ' + db_name + ' .journal_mode = WAL; ' )
2017-07-12 20:03:45 +00:00
# if this is set to 1, transactions are not immediately synced to the journal and can be undone following a power-loss
# if set to 2, all transactions are synced
# either way, transactions are atomically consistent, but let's not mess around when power-cut during heavy file import or w/e
2017-06-14 21:19:11 +00:00
self . _c . execute ( ' PRAGMA ' + db_name + ' .synchronous = 2; ' )
2016-04-14 01:54:29 +00:00
try :
2016-01-20 23:57:33 +00:00
2016-04-14 01:54:29 +00:00
self . _c . execute ( ' SELECT * FROM ' + db_name + ' .sqlite_master; ' ) . fetchone ( )
2016-01-20 23:57:33 +00:00
2019-03-20 21:22:10 +00:00
except sqlite3 . OperationalError as e :
2016-01-20 23:57:33 +00:00
2019-03-20 21:22:10 +00:00
message = ' The database failed to read some data. You may need to run the program in no-wal mode using the --no_wal command parameter. Full error information: '
message + = os . linesep * 2
message + = str ( e )
2016-01-20 23:57:33 +00:00
2019-03-20 21:22:10 +00:00
HydrusData . DebugPrint ( message )
2016-01-20 23:57:33 +00:00
2019-03-20 21:22:10 +00:00
raise HydrusExceptions . DBAccessException ( message )
2016-01-20 23:57:33 +00:00
2016-01-13 22:08:19 +00:00
2015-04-22 22:57:25 +00:00
2017-07-12 20:03:45 +00:00
try :
self . _BeginImmediate ( )
except Exception as e :
2019-01-09 22:59:03 +00:00
raise HydrusExceptions . DBAccessException ( str ( e ) )
2017-07-12 20:03:45 +00:00
2015-04-22 22:57:25 +00:00
2017-03-29 19:39:34 +00:00
def _InitDiskCache ( self ) :
pass
2016-04-20 20:42:21 +00:00
def _InitExternalDatabases ( self ) :
pass
2015-04-22 22:57:25 +00:00
def _ManageDBError ( self , job , e ) :
raise NotImplementedError ( )
def _ProcessJob ( self , job ) :
job_type = job . GetType ( )
2016-03-30 22:56:50 +00:00
( action , args , kwargs ) = job . GetCallableTuple ( )
2015-04-22 22:57:25 +00:00
try :
2016-04-20 20:42:21 +00:00
if job_type in ( ' read_write ' , ' write ' ) :
2017-05-24 20:28:24 +00:00
self . _current_status = ' db write locked '
2017-07-12 20:03:45 +00:00
self . _transaction_contains_writes = True
2016-04-20 20:42:21 +00:00
2017-05-24 20:28:24 +00:00
else :
self . _current_status = ' db read locked '
2017-07-05 21:09:28 +00:00
self . publish_status_update ( )
2015-04-22 22:57:25 +00:00
2017-05-24 20:28:24 +00:00
if job_type in ( ' read ' , ' read_write ' ) :
result = self . _Read ( action , * args , * * kwargs )
elif job_type in ( ' write ' ) :
result = self . _Write ( action , * args , * * kwargs )
2015-04-22 22:57:25 +00:00
2017-11-01 20:37:39 +00:00
if self . _transaction_contains_writes and HydrusData . TimeHasPassed ( self . _transaction_started + self . TRANSACTION_COMMIT_TIME ) :
2016-04-20 20:42:21 +00:00
2017-05-24 20:28:24 +00:00
self . _current_status = ' db committing '
2017-07-05 21:09:28 +00:00
self . publish_status_update ( )
2017-05-24 20:28:24 +00:00
2017-03-29 19:39:34 +00:00
self . _Commit ( )
2016-04-20 20:42:21 +00:00
2017-07-12 20:03:45 +00:00
self . _BeginImmediate ( )
2019-02-27 23:03:30 +00:00
self . _transaction_contains_writes = False
2017-07-12 20:03:45 +00:00
else :
self . _Save ( )
2015-04-22 22:57:25 +00:00
2016-03-30 22:56:50 +00:00
for ( topic , args , kwargs ) in self . _pubsubs :
self . _controller . pub ( topic , * args , * * kwargs )
2015-04-22 22:57:25 +00:00
2016-03-30 22:56:50 +00:00
if job . IsSynchronous ( ) :
job . PutResult ( result )
2015-04-22 22:57:25 +00:00
except Exception as e :
2018-02-28 22:30:36 +00:00
self . _ManageDBError ( job , e )
2017-07-12 20:03:45 +00:00
try :
2016-04-20 20:42:21 +00:00
2017-07-12 20:03:45 +00:00
self . _Rollback ( )
except Exception as rollback_e :
2018-02-28 22:30:36 +00:00
HydrusData . Print ( ' When the transaction failed, attempting to rollback the database failed. Please restart the client as soon as is convenient. ' )
self . _in_transaction = False
self . _CloseDBCursor ( )
self . _InitDBCursor ( )
2017-07-12 20:03:45 +00:00
HydrusData . PrintException ( rollback_e )
2016-04-20 20:42:21 +00:00
2015-04-22 22:57:25 +00:00
2017-05-24 20:28:24 +00:00
finally :
2017-07-12 20:03:45 +00:00
self . _pubsubs = [ ]
2017-05-24 20:28:24 +00:00
self . _current_status = ' '
2017-07-05 21:09:28 +00:00
self . publish_status_update ( )
2017-05-24 20:28:24 +00:00
2015-04-22 22:57:25 +00:00
def _Read ( self , action , * args , * * kwargs ) :
raise NotImplementedError ( )
2018-01-17 22:52:10 +00:00
def _RepairDB ( self ) :
pass
2018-11-28 22:31:04 +00:00
def _ReportOverupdatedDB ( self , version ) :
pass
2019-02-13 22:26:43 +00:00
def _ReportUnderupdatedDB ( self , version ) :
pass
2015-04-22 22:57:25 +00:00
def _ReportStatus ( self , text ) :
2015-11-18 22:44:07 +00:00
HydrusData . Print ( text )
2015-04-22 22:57:25 +00:00
2017-03-29 19:39:34 +00:00
def _Rollback ( self ) :
if self . _in_transaction :
2017-07-12 20:03:45 +00:00
self . _c . execute ( ' ROLLBACK TO hydrus_savepoint; ' )
2017-03-29 19:39:34 +00:00
else :
HydrusData . Print ( ' Received a call to rollback, but was not in a transaction! ' )
2017-07-12 20:03:45 +00:00
def _Save ( self ) :
self . _c . execute ( ' RELEASE hydrus_savepoint; ' )
self . _c . execute ( ' SAVEPOINT hydrus_savepoint; ' )
2017-01-25 22:56:55 +00:00
def _SelectFromList ( self , select_statement , xs ) :
# issue here is that doing a simple blah_id = ? is real quick and cacheable but doing a lot of fetchone()s is slow
# blah_id IN ( 1, 2, 3 ) is fast to execute but not cacheable and doing the str() list splay takes time so there is initial lag
# doing the temporaryintegertable trick works well for gigantic lists you refer to frequently but it is super laggy when you sometimes are only selecting four things
# blah_id IN ( ?, ?, ? ) is fast and cacheable but there's a small limit (1024 is too many) to the number of params sql can handle
# so lets do the latter but break it into 256-strong chunks to get a good medium
# this will take a select statement with %s like so:
# SELECT blah_id, blah FROM blahs WHERE blah_id IN %s;
MAX_CHUNK_SIZE = 256
# do this just so we aren't always reproducing this long string for gigantic lists
# and also so we aren't overmaking it when this gets spammed with a lot of len() == 1 calls
if len ( xs ) > = MAX_CHUNK_SIZE :
max_statement = select_statement % ( ' ( ' + ' , ' . join ( ' ? ' * MAX_CHUNK_SIZE ) + ' ) ' )
for chunk in HydrusData . SplitListIntoChunks ( xs , MAX_CHUNK_SIZE ) :
if len ( chunk ) == MAX_CHUNK_SIZE :
chunk_statement = max_statement
else :
chunk_statement = select_statement % ( ' ( ' + ' , ' . join ( ' ? ' * len ( chunk ) ) + ' ) ' )
for row in self . _c . execute ( chunk_statement , chunk ) :
yield row
2017-02-08 22:27:00 +00:00
def _SelectFromListFetchAll ( self , select_statement , xs ) :
return [ row for row in self . _SelectFromList ( select_statement , xs ) ]
2019-02-27 23:03:30 +00:00
def _ShrinkMemory ( self ) :
self . _c . execute ( ' PRAGMA shrink_memory; ' )
2017-03-15 20:13:04 +00:00
def _STI ( self , iterable_cursor ) :
# strip singleton tuples to an iterator
return ( item for ( item , ) in iterable_cursor )
2017-03-08 23:23:12 +00:00
def _STL ( self , iterable_cursor ) :
# strip singleton tuples to a list
return [ item for ( item , ) in iterable_cursor ]
def _STS ( self , iterable_cursor ) :
# strip singleton tuples to a set
return { item for ( item , ) in iterable_cursor }
2015-04-22 22:57:25 +00:00
def _UpdateDB ( self , version ) :
raise NotImplementedError ( )
def _Write ( self , action , * args , * * kwargs ) :
raise NotImplementedError ( )
2017-07-12 20:03:45 +00:00
def pub_after_job ( self , topic , * args , * * kwargs ) :
2016-03-30 22:56:50 +00:00
2019-01-23 22:19:16 +00:00
if len ( args ) == 0 and len ( kwargs ) == 0 :
if ( topic , args , kwargs ) in self . _pubsubs :
return
2016-03-30 22:56:50 +00:00
self . _pubsubs . append ( ( topic , args , kwargs ) )
2015-04-22 22:57:25 +00:00
2017-07-05 21:09:28 +00:00
def publish_status_update ( self ) :
pass
2015-06-03 21:05:13 +00:00
def CurrentlyDoingJob ( self ) :
return self . _currently_doing_job
2017-07-12 20:03:45 +00:00
def GetApproxTotalFileSize ( self ) :
total = 0
2019-01-09 22:59:03 +00:00
for filename in list ( self . _db_filenames . values ( ) ) :
2017-07-12 20:03:45 +00:00
path = os . path . join ( self . _db_dir , filename )
total + = os . path . getsize ( path )
return total
2017-05-24 20:28:24 +00:00
def GetStatus ( self ) :
return ( self . _current_status , self . _current_job_name )
2016-08-31 19:55:14 +00:00
def IsDBUpdated ( self ) :
return self . _is_db_updated
def IsFirstStart ( self ) :
return self . _is_first_start
2016-03-30 22:56:50 +00:00
def LoopIsFinished ( self ) :
return self . _loop_finished
2015-04-22 22:57:25 +00:00
2016-04-06 19:52:45 +00:00
def JobsQueueEmpty ( self ) :
return self . _jobs . empty ( )
2015-04-22 22:57:25 +00:00
def MainLoop ( self ) :
2016-03-16 22:19:14 +00:00
try :
self . _InitDBCursor ( ) # have to reinitialise because the thread id has changed
2017-03-29 19:39:34 +00:00
self . _InitDiskCache ( )
2016-03-16 22:19:14 +00:00
self . _InitCaches ( )
2016-04-06 19:52:45 +00:00
except :
2016-03-16 22:19:14 +00:00
2018-02-14 21:47:18 +00:00
self . _DisplayCatastrophicError ( traceback . format_exc ( ) )
2016-03-16 22:19:14 +00:00
self . _could_not_initialise = True
2016-04-06 19:52:45 +00:00
return
2015-04-22 22:57:25 +00:00
2016-03-16 22:19:14 +00:00
self . _ready_to_serve_requests = True
2015-04-29 19:20:35 +00:00
2015-04-22 22:57:25 +00:00
error_count = 0
2015-11-04 22:30:28 +00:00
while not ( ( self . _local_shutdown or self . _controller . ModelIsShutdown ( ) ) and self . _jobs . empty ( ) ) :
2015-04-22 22:57:25 +00:00
try :
2019-01-09 22:59:03 +00:00
job = self . _jobs . get ( timeout = 1 )
2015-04-22 22:57:25 +00:00
self . _currently_doing_job = True
2017-05-24 20:28:24 +00:00
self . _current_job_name = job . ToString ( )
2015-04-22 22:57:25 +00:00
2017-07-05 21:09:28 +00:00
self . publish_status_update ( )
2015-06-03 21:05:13 +00:00
2015-04-22 22:57:25 +00:00
try :
2018-09-05 20:52:32 +00:00
if HG . db_report_mode :
summary = ' Running ' + job . ToString ( )
HydrusData . ShowText ( summary )
2017-05-10 21:33:58 +00:00
if HG . db_profile_mode :
2015-04-22 22:57:25 +00:00
2017-03-08 23:23:12 +00:00
summary = ' Profiling ' + job . ToString ( )
HydrusData . ShowText ( summary )
2015-04-22 22:57:25 +00:00
2017-03-08 23:23:12 +00:00
HydrusData . Profile ( summary , ' self._ProcessJob( job ) ' , globals ( ) , locals ( ) )
2015-04-22 22:57:25 +00:00
else :
self . _ProcessJob ( job )
error_count = 0
except :
error_count + = 1
2017-08-02 21:32:54 +00:00
if error_count > 5 :
raise
2015-04-22 22:57:25 +00:00
2019-01-09 22:59:03 +00:00
self . _jobs . put ( job ) # couldn't lock db; put job back on queue
2015-04-22 22:57:25 +00:00
time . sleep ( 5 )
self . _currently_doing_job = False
2017-05-24 20:28:24 +00:00
self . _current_job_name = ' '
2015-04-22 22:57:25 +00:00
2017-07-05 21:09:28 +00:00
self . publish_status_update ( )
2015-06-03 21:05:13 +00:00
2019-01-09 22:59:03 +00:00
except queue . Empty :
2016-03-30 22:56:50 +00:00
2017-11-01 20:37:39 +00:00
if self . _transaction_contains_writes and HydrusData . TimeHasPassed ( self . _transaction_started + self . TRANSACTION_COMMIT_TIME ) :
2017-08-02 21:32:54 +00:00
self . _Commit ( )
self . _BeginImmediate ( )
2019-02-27 23:03:30 +00:00
self . _transaction_contains_writes = False
2016-03-30 22:56:50 +00:00
2015-06-17 20:01:41 +00:00
2016-04-20 20:42:21 +00:00
if HydrusData . TimeHasPassed ( self . _connection_timestamp + CONNECTION_REFRESH_TIME ) : # just to clear out the journal files
self . _InitDBCursor ( )
2015-06-17 20:01:41 +00:00
self . _CleanUpCaches ( )
2015-04-22 22:57:25 +00:00
self . _CloseDBCursor ( )
self . _loop_finished = True
2019-01-09 22:59:03 +00:00
def Read ( self , action , * args , * * kwargs ) :
2015-04-22 22:57:25 +00:00
2019-01-09 22:59:03 +00:00
if action in self . READ_WRITE_ACTIONS :
job_type = ' read_write '
else :
job_type = ' read '
2015-04-22 22:57:25 +00:00
synchronous = True
2016-03-30 22:56:50 +00:00
job = HydrusData . JobDatabase ( job_type , synchronous , action , * args , * * kwargs )
2015-04-22 22:57:25 +00:00
2015-11-04 22:30:28 +00:00
if self . _controller . ModelIsShutdown ( ) :
raise HydrusExceptions . ShutdownException ( ' Application has shut down! ' )
2015-04-22 22:57:25 +00:00
2019-01-09 22:59:03 +00:00
self . _jobs . put ( job )
2015-04-22 22:57:25 +00:00
2016-03-16 22:19:14 +00:00
return job . GetResult ( )
def ReadyToServeRequests ( self ) :
return self . _ready_to_serve_requests
2015-04-22 22:57:25 +00:00
2016-03-30 22:56:50 +00:00
def Shutdown ( self ) :
self . _local_shutdown = True
2015-04-22 22:57:25 +00:00
2019-01-09 22:59:03 +00:00
def Write ( self , action , synchronous , * args , * * kwargs ) :
2015-04-22 22:57:25 +00:00
2016-03-30 22:56:50 +00:00
job_type = ' write '
2015-04-22 22:57:25 +00:00
2016-03-30 22:56:50 +00:00
job = HydrusData . JobDatabase ( job_type , synchronous , action , * args , * * kwargs )
2015-04-22 22:57:25 +00:00
2015-11-04 22:30:28 +00:00
if self . _controller . ModelIsShutdown ( ) :
raise HydrusExceptions . ShutdownException ( ' Application has shut down! ' )
2015-04-22 22:57:25 +00:00
2019-01-09 22:59:03 +00:00
self . _jobs . put ( job )
2015-04-22 22:57:25 +00:00
if synchronous : return job . GetResult ( )
2016-09-28 18:48:01 +00:00
class TemporaryIntegerTable ( object ) :
def __init__ ( self , cursor , integer_iterable , column_name ) :
self . _cursor = cursor
self . _integer_iterable = integer_iterable
self . _column_name = column_name
2019-01-09 22:59:03 +00:00
self . _table_name = ' mem.tempint ' + os . urandom ( 32 ) . hex ( )
2016-09-28 18:48:01 +00:00
def __enter__ ( self ) :
self . _cursor . execute ( ' CREATE TABLE ' + self . _table_name + ' ( ' + self . _column_name + ' INTEGER PRIMARY KEY ); ' )
self . _cursor . executemany ( ' INSERT INTO ' + self . _table_name + ' ( ' + self . _column_name + ' ) VALUES ( ? ); ' , ( ( i , ) for i in self . _integer_iterable ) )
return self . _table_name
def __exit__ ( self , exc_type , exc_val , exc_tb ) :
self . _cursor . execute ( ' DROP TABLE ' + self . _table_name + ' ; ' )
return False
2017-01-25 22:56:55 +00:00