mirror of https://github.com/citusdata/citus.git
Allow Citus local tables with 2PC
parent
07d3b4fd04
commit
442449c4bf
|
@ -618,6 +618,7 @@ static bool DistributedExecutionModifiesDatabase(DistributedExecution *execution
|
||||||
static bool IsMultiShardModification(RowModifyLevel modLevel, List *taskList);
|
static bool IsMultiShardModification(RowModifyLevel modLevel, List *taskList);
|
||||||
static bool TaskListModifiesDatabase(RowModifyLevel modLevel, List *taskList);
|
static bool TaskListModifiesDatabase(RowModifyLevel modLevel, List *taskList);
|
||||||
static bool DistributedExecutionRequiresRollback(List *taskList);
|
static bool DistributedExecutionRequiresRollback(List *taskList);
|
||||||
|
static bool TaskAccessesOnlyCitusLocalTables(Task *task);
|
||||||
static bool TaskListRequires2PC(List *taskList);
|
static bool TaskListRequires2PC(List *taskList);
|
||||||
static bool SelectForUpdateOnReferenceTable(List *taskList);
|
static bool SelectForUpdateOnReferenceTable(List *taskList);
|
||||||
static void AssignTasksToConnectionsOrWorkerPool(DistributedExecution *execution);
|
static void AssignTasksToConnectionsOrWorkerPool(DistributedExecution *execution);
|
||||||
|
@ -1165,23 +1166,6 @@ DecideTransactionPropertiesForTaskList(RowModifyLevel modLevel, List *taskList,
|
||||||
return xactProperties;
|
return xactProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GetCurrentLocalExecutionStatus() == LOCAL_EXECUTION_REQUIRED)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* In case localExecutionHappened, we force the executor to use 2PC.
|
|
||||||
* The primary motivation is that at this point we're definitely expanding
|
|
||||||
* the nodes participated in the transaction. And, by re-generating the
|
|
||||||
* remote task lists during local query execution, we might prevent the adaptive
|
|
||||||
* executor to kick-in 2PC (or even start coordinated transaction, that's why
|
|
||||||
* we prefer adding this check here instead of
|
|
||||||
* Activate2PCIfModifyingTransactionExpandsToNewNode()).
|
|
||||||
*/
|
|
||||||
xactProperties.errorOnAnyFailure = true;
|
|
||||||
xactProperties.useRemoteTransactionBlocks = TRANSACTION_BLOCKS_REQUIRED;
|
|
||||||
xactProperties.requires2PC = true;
|
|
||||||
return xactProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DistributedExecutionRequiresRollback(taskList))
|
if (DistributedExecutionRequiresRollback(taskList))
|
||||||
{
|
{
|
||||||
/* transaction blocks are required if the task list needs to roll back */
|
/* transaction blocks are required if the task list needs to roll back */
|
||||||
|
@ -1361,6 +1345,25 @@ DistributedExecutionRequiresRollback(List *taskList)
|
||||||
|
|
||||||
Task *task = (Task *) linitial(taskList);
|
Task *task = (Task *) linitial(taskList);
|
||||||
|
|
||||||
|
if (taskCount == 1 && TaskAccessesOnlyCitusLocalTables(task) &&
|
||||||
|
ShouldExecuteTasksLocally(taskList))
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* If the execution accesses Citus local tables via local execution,
|
||||||
|
* there is no need to start a coordinated transaction. This is
|
||||||
|
* especially important regarding the limitations of coordinated
|
||||||
|
* transactions such as a coordinated transaction cannot be part of
|
||||||
|
* a 2PC transaction.
|
||||||
|
*
|
||||||
|
* NB: It is not precisely accurate to rely on ShouldExecuteTasksLocally()
|
||||||
|
* at this point as remote execution may failover some tasks to local
|
||||||
|
* execution. However, we prefer this approach because it provides a more
|
||||||
|
* predictable user experience.
|
||||||
|
*/
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool selectForUpdate = task->relationRowLockList != NIL;
|
bool selectForUpdate = task->relationRowLockList != NIL;
|
||||||
if (selectForUpdate)
|
if (selectForUpdate)
|
||||||
{
|
{
|
||||||
|
@ -1423,6 +1426,27 @@ DistributedExecutionRequiresRollback(List *taskList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TaskAccessesOnlyCitusLocalTable returns true if the input task
|
||||||
|
* has only accesses Citus local tables.
|
||||||
|
*/
|
||||||
|
static bool
|
||||||
|
TaskAccessesOnlyCitusLocalTables(Task *task)
|
||||||
|
{
|
||||||
|
List *relationShardList = task->relationShardList;
|
||||||
|
RelationShard *relationShard = NULL;
|
||||||
|
foreach_ptr(relationShard, relationShardList)
|
||||||
|
{
|
||||||
|
if (!IsCitusTableType(relationShard->relationId, CITUS_LOCAL_TABLE))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TaskListRequires2PC determines whether the given task list requires 2PC
|
* TaskListRequires2PC determines whether the given task list requires 2PC
|
||||||
* because the tasks provided operates on a reference table or there are multiple
|
* because the tasks provided operates on a reference table or there are multiple
|
||||||
|
@ -3199,6 +3223,11 @@ Activate2PCIfModifyingTransactionExpandsToNewNode(WorkerSession *session)
|
||||||
*/
|
*/
|
||||||
CoordinatedTransactionUse2PC();
|
CoordinatedTransactionUse2PC();
|
||||||
}
|
}
|
||||||
|
else if (GetCurrentLocalExecutionStatus() == LOCAL_EXECUTION_REQUIRED)
|
||||||
|
{
|
||||||
|
/* we did local execution and are expanding to an additional node */
|
||||||
|
CoordinatedTransactionUse2PC();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1015,6 +1015,82 @@ BEGIN;
|
||||||
(0 rows)
|
(0 rows)
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
-- users are not allowed to use
|
||||||
|
-- citus local tables in prepared statements
|
||||||
|
-- from the worker nodes
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO citus_local_table VALUES (3), (4);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM citus_local_table;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
2
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM reference_table;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT count(*) FROM citus_local_table;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
2
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM citus_local_table;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
2
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT count(*) FROM reference_table;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM citus_local_table;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
2
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT count(*) FROM distributed_table;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM distributed_table;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT count(*) FROM citus_local_table;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
2
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
\c - - - :master_port
|
\c - - - :master_port
|
||||||
-- cleanup at exit
|
-- cleanup at exit
|
||||||
DROP SCHEMA citus_local_table_queries_mx CASCADE;
|
DROP SCHEMA citus_local_table_queries_mx CASCADE;
|
||||||
|
|
|
@ -1207,6 +1207,128 @@ SELECT citus_add_local_table_to_metadata('local_table_2', cascade_via_foreign_ke
|
||||||
|
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
|
CREATE TABLE distributed_table_tx_blck (col_1 INT UNIQUE);
|
||||||
|
SELECT create_distributed_table('distributed_table_tx_blck', 'col_1');
|
||||||
|
create_distributed_table
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
CREATE TABLE ref_table_tx_blck (col_1 INT UNIQUE);
|
||||||
|
SELECT create_reference_table('ref_table_tx_blck');
|
||||||
|
create_reference_table
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- make sure that we can do 2PC with a citus local table
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ROLLBACK PREPARED 'citus_local_tx';
|
||||||
|
-- make sure that we can do 2PC with citus local tables
|
||||||
|
-- multiple statements
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
INSERT INTO local_table_1 VALUES (2);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ROLLBACK PREPARED 'citus_local_tx';
|
||||||
|
-- make sure that we cannot do 2PC with citus local tables
|
||||||
|
-- when local execution is disabled
|
||||||
|
BEGIN;
|
||||||
|
SET citus.enable_local_execution TO false;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
INSERT INTO local_table_1 VALUES (2);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
-- make sure that we cannot do 2PC with citus local tables
|
||||||
|
-- when it is used with distributed tables on the tx block
|
||||||
|
BEGIN;
|
||||||
|
SET LOCAL citus.multi_shard_modify_mode TO 'sequential';
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
SELECT count(*) FROM distributed_table_tx_blck;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
-- make sure that we cannot do 2PC with citus local tables
|
||||||
|
-- when it is used with distributed tables on the tx block
|
||||||
|
BEGIN;
|
||||||
|
SET LOCAL citus.multi_shard_modify_mode TO 'sequential';
|
||||||
|
SELECT count(*) FROM distributed_table_tx_blck;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
-- make sure that we cannot do 2PC with citus local tables
|
||||||
|
-- when it is used with distributed tables on the tx block
|
||||||
|
-- even if local execution is disabled
|
||||||
|
BEGIN;
|
||||||
|
SET citus.enable_local_execution TO false;
|
||||||
|
SET LOCAL citus.multi_shard_modify_mode TO 'sequential';
|
||||||
|
SELECT count(*) FROM distributed_table_tx_blck;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
-- make sure that we cannot do 2PC with citus local
|
||||||
|
-- tables and reference tables
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM ref_table_tx_blck;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
SELECT count(*) FROM ref_table_tx_blck;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM local_table_1;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT count(*) FROM ref_table_tx_blck;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO ref_table_tx_blck VALUES (1000);
|
||||||
|
SELECT count(*) FROM local_table_1;
|
||||||
|
count
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
0
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ERROR: cannot use 2PC in transactions involving multiple servers
|
||||||
CREATE PROCEDURE call_delegation(x int) LANGUAGE plpgsql AS $$
|
CREATE PROCEDURE call_delegation(x int) LANGUAGE plpgsql AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO test (x) VALUES ($1);
|
INSERT INTO test (x) VALUES ($1);
|
||||||
|
|
|
@ -658,6 +658,33 @@ BEGIN;
|
||||||
SELECT * FROM reference_table ORDER BY 1,2;
|
SELECT * FROM reference_table ORDER BY 1,2;
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
||||||
|
-- users are not allowed to use
|
||||||
|
-- citus local tables in prepared statements
|
||||||
|
-- from the worker nodes
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO citus_local_table VALUES (3), (4);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM citus_local_table;
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM reference_table;
|
||||||
|
SELECT count(*) FROM citus_local_table;
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM citus_local_table;
|
||||||
|
SELECT count(*) FROM reference_table;
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM citus_local_table;
|
||||||
|
SELECT count(*) FROM distributed_table;
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM distributed_table;
|
||||||
|
SELECT count(*) FROM citus_local_table;
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx_on_mx';
|
||||||
|
|
||||||
|
|
||||||
\c - - - :master_port
|
\c - - - :master_port
|
||||||
-- cleanup at exit
|
-- cleanup at exit
|
||||||
DROP SCHEMA citus_local_table_queries_mx CASCADE;
|
DROP SCHEMA citus_local_table_queries_mx CASCADE;
|
||||||
|
|
|
@ -654,6 +654,81 @@ ALTER TABLE local_table_1 ADD CONSTRAINT fkey_8 FOREIGN KEY (col_1) REFERENCES l
|
||||||
|
|
||||||
SELECT citus_add_local_table_to_metadata('local_table_2', cascade_via_foreign_keys=>true);
|
SELECT citus_add_local_table_to_metadata('local_table_2', cascade_via_foreign_keys=>true);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE distributed_table_tx_blck (col_1 INT UNIQUE);
|
||||||
|
SELECT create_distributed_table('distributed_table_tx_blck', 'col_1');
|
||||||
|
CREATE TABLE ref_table_tx_blck (col_1 INT UNIQUE);
|
||||||
|
SELECT create_reference_table('ref_table_tx_blck');
|
||||||
|
|
||||||
|
-- make sure that we can do 2PC with a citus local table
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ROLLBACK PREPARED 'citus_local_tx';
|
||||||
|
|
||||||
|
-- make sure that we can do 2PC with citus local tables
|
||||||
|
-- multiple statements
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
INSERT INTO local_table_1 VALUES (2);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
ROLLBACK PREPARED 'citus_local_tx';
|
||||||
|
|
||||||
|
-- make sure that we cannot do 2PC with citus local tables
|
||||||
|
-- when local execution is disabled
|
||||||
|
BEGIN;
|
||||||
|
SET citus.enable_local_execution TO false;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
INSERT INTO local_table_1 VALUES (2);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
|
||||||
|
-- make sure that we cannot do 2PC with citus local tables
|
||||||
|
-- when it is used with distributed tables on the tx block
|
||||||
|
BEGIN;
|
||||||
|
SET LOCAL citus.multi_shard_modify_mode TO 'sequential';
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
SELECT count(*) FROM distributed_table_tx_blck;
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
|
||||||
|
-- make sure that we cannot do 2PC with citus local tables
|
||||||
|
-- when it is used with distributed tables on the tx block
|
||||||
|
BEGIN;
|
||||||
|
SET LOCAL citus.multi_shard_modify_mode TO 'sequential';
|
||||||
|
SELECT count(*) FROM distributed_table_tx_blck;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
|
||||||
|
-- make sure that we cannot do 2PC with citus local tables
|
||||||
|
-- when it is used with distributed tables on the tx block
|
||||||
|
-- even if local execution is disabled
|
||||||
|
BEGIN;
|
||||||
|
SET citus.enable_local_execution TO false;
|
||||||
|
SET LOCAL citus.multi_shard_modify_mode TO 'sequential';
|
||||||
|
SELECT count(*) FROM distributed_table_tx_blck;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
|
||||||
|
|
||||||
|
-- make sure that we cannot do 2PC with citus local
|
||||||
|
-- tables and reference tables
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM ref_table_tx_blck;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO local_table_1 VALUES (1);
|
||||||
|
SELECT count(*) FROM ref_table_tx_blck;
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
BEGIN;
|
||||||
|
SELECT count(*) FROM local_table_1;
|
||||||
|
SELECT count(*) FROM ref_table_tx_blck;
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
BEGIN;
|
||||||
|
INSERT INTO ref_table_tx_blck VALUES (1000);
|
||||||
|
SELECT count(*) FROM local_table_1;
|
||||||
|
PREPARE TRANSACTION 'citus_local_tx';
|
||||||
|
|
||||||
|
|
||||||
CREATE PROCEDURE call_delegation(x int) LANGUAGE plpgsql AS $$
|
CREATE PROCEDURE call_delegation(x int) LANGUAGE plpgsql AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO test (x) VALUES ($1);
|
INSERT INTO test (x) VALUES ($1);
|
||||||
|
|
Loading…
Reference in New Issue