1+ import sys
12from contextlib import contextmanager
23from time import sleep
34
45from django .db .backends .postgresql .creation import DatabaseCreation as PostGISDatabaseCreation
56
67
78class DatabaseCreation (PostGISDatabaseCreation ):
9+ _hold_restore_mode_for_parallel = False
10+ _block_source_database_connections = False
11+
812 @contextmanager
913 def _source_database_cursor (self ):
1014 self .connection .close ()
@@ -14,55 +18,123 @@ def _source_database_cursor(self):
1418 finally :
1519 self .connection .close ()
1620
17- def _enter_timescaledb_restore_mode (self ):
21+ def _parallel_test_processes_requested (self ):
22+ args = sys .argv [1 :]
23+ for index , arg in enumerate (args ):
24+ if arg == "--parallel" :
25+ if index + 1 >= len (args ) or args [index + 1 ].startswith ("-" ):
26+ return 2
27+ value = args [index + 1 ]
28+ elif arg .startswith ("--parallel=" ):
29+ value = arg .split ("=" , 1 )[1 ]
30+ else :
31+ continue
32+
33+ if value == "auto" :
34+ return 2
35+ try :
36+ return int (value )
37+ except ValueError :
38+ return 0
39+ return 0
40+
41+ def _source_database_name (self ):
42+ return self .connection .settings_dict ["NAME" ]
43+
44+ def _timescaledb_extension_installed (self ):
1845 with self ._source_database_cursor () as cursor :
1946 cursor .execute ("SELECT 1 FROM pg_extension WHERE extname = 'timescaledb'" )
20- if cursor .fetchone () is None :
21- return False
47+ return cursor .fetchone () is not None
2248
49+ def _timescaledb_restore_mode_enabled (self ):
50+ with self ._source_database_cursor () as cursor :
2351 cursor .execute ("SHOW timescaledb.restoring" )
24- if cursor .fetchone ()[0 ].lower () == "on" :
25- return False
52+ return cursor .fetchone ()[0 ].lower () == "on"
2653
54+ def _enter_timescaledb_restore_mode (self ):
55+ if not self ._timescaledb_extension_installed ():
56+ return False
57+ if self ._timescaledb_restore_mode_enabled ():
58+ return False
59+
60+ with self ._source_database_cursor () as cursor :
2761 # Timescale background workers attach to the template database and
2862 # block CREATE DATABASE ... WITH TEMPLATE during parallel test setup.
2963 cursor .execute ("SELECT timescaledb_pre_restore()" )
30- return True
64+ return True
3165
3266 def _exit_timescaledb_restore_mode (self ):
67+ if not self ._timescaledb_extension_installed ():
68+ return
69+ if not self ._timescaledb_restore_mode_enabled ():
70+ return
71+
3372 with self ._source_database_cursor () as cursor :
34- cursor .execute ("SHOW timescaledb.restoring" )
35- if cursor .fetchone ()[0 ].lower () == "on" :
36- cursor .execute ("SELECT timescaledb_post_restore()" )
73+ cursor .execute ("SELECT timescaledb_post_restore()" )
74+
75+ def _set_source_database_allow_connections (self , allow_connections ):
76+ source_database_name = self ._source_database_name ()
77+ quoted_source_database_name = self ._quote_name (source_database_name )
78+ allow_connections_sql = "true" if allow_connections else "false"
79+
80+ with self ._nodb_cursor () as cursor :
81+ cursor .execute (f"ALTER DATABASE { quoted_source_database_name } WITH ALLOW_CONNECTIONS { allow_connections_sql } " )
3782
3883 def _disconnect_source_database_sessions (self ):
39- with self ._source_database_cursor () as cursor :
84+ source_database_name = self ._source_database_name ()
85+ with self ._nodb_cursor () as cursor :
4086 for _ in range (20 ):
4187 cursor .execute (
4288 """
4389 SELECT pg_terminate_backend(pid)
4490 FROM pg_stat_activity
45- WHERE datname = current_database()
91+ WHERE datname = %s
4692 AND pid <> pg_backend_pid()
47- """
93+ """ ,
94+ [source_database_name ],
4895 )
4996 cursor .execute (
5097 """
5198 SELECT COUNT(*)
5299 FROM pg_stat_activity
53- WHERE datname = current_database()
100+ WHERE datname = %s
54101 AND pid <> pg_backend_pid()
55- """
102+ """ ,
103+ [source_database_name ],
56104 )
57105 if cursor .fetchone ()[0 ] == 0 :
58106 return
59107 sleep (0.25 )
60108
61- def _clone_test_db (self , suffix , verbosity , keepdb = False ):
62- restore_mode_changed = self ._enter_timescaledb_restore_mode ()
63- try :
109+ def create_test_db (self , verbosity = 1 , autoclobber = False , serialize = True , keepdb = False ):
110+ test_database_name = super ().create_test_db (
111+ verbosity = verbosity ,
112+ autoclobber = autoclobber ,
113+ serialize = serialize ,
114+ keepdb = keepdb ,
115+ )
116+
117+ if self ._parallel_test_processes_requested () > 1 :
118+ self ._hold_restore_mode_for_parallel = self ._enter_timescaledb_restore_mode () or self ._timescaledb_restore_mode_enabled ()
119+ self ._set_source_database_allow_connections (False )
120+ self ._block_source_database_connections = True
64121 self ._disconnect_source_database_sessions ()
65- super ()._clone_test_db (suffix , verbosity , keepdb )
66- finally :
67- if restore_mode_changed :
122+
123+ return test_database_name
124+
125+ def _clone_test_db (self , suffix , verbosity , keepdb = False ):
126+ if not self ._hold_restore_mode_for_parallel :
127+ self ._enter_timescaledb_restore_mode ()
128+ self ._disconnect_source_database_sessions ()
129+ super ()._clone_test_db (suffix , verbosity , keepdb )
130+
131+ def destroy_test_db (self , old_database_name = None , verbosity = 1 , keepdb = False , suffix = None ):
132+ if suffix is None :
133+ if self ._block_source_database_connections and keepdb :
134+ self ._set_source_database_allow_connections (True )
135+ self ._block_source_database_connections = False
136+
137+ if keepdb and self ._hold_restore_mode_for_parallel and self ._timescaledb_extension_installed ():
68138 self ._exit_timescaledb_restore_mode ()
139+ self ._hold_restore_mode_for_parallel = False
140+ super ().destroy_test_db (old_database_name , verbosity , keepdb , suffix )
0 commit comments