Skip to content

Commit df1e2ff

Browse files
authored
Prevent CREATE TABLE from using dangling tablespace (#876)
When DROP TABLESPACE is running, it's possible to still use the dropping tablespace in CREATE TABLE. It's bad behavior that the table may use a dropped tablespace, which means that data files are stored out of the database. This commit controls creating files in a tablespace and dropping tablespace running in exclusive mode. The key timelines for dropping a tablespace is: 1. Lock tuple of pg_tablespace in AccessExclusive mode. 2. CommitTransaction. 3. Remove directories of the tablespace. 4. Release the lock of the tablespace tuple.
1 parent 7618411 commit df1e2ff

8 files changed

Lines changed: 351 additions & 81 deletions

File tree

src/backend/access/transam/xact.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2995,6 +2995,8 @@ CommitTransaction(void)
29952995

29962996
AtEOXact_MultiXact();
29972997

2998+
AtCommit_TablespaceStorage();
2999+
29983000
ResourceOwnerRelease(TopTransactionResourceOwner,
29993001
RESOURCE_RELEASE_LOCKS,
30003002
true, true);
@@ -3026,8 +3028,6 @@ CommitTransaction(void)
30263028
if(Gp_role == GP_ROLE_DISPATCH || IS_SINGLENODE())
30273029
MoveDbSessionLockRelease();
30283030

3029-
AtCommit_TablespaceStorage();
3030-
30313031
/*
30323032
* Send out notification signals to other backends (and do other
30333033
* post-commit NOTIFY cleanup). This must not happen until after our

src/backend/commands/tablespace.c

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,24 @@ static void ensure_tablespace_directory_is_empty(const Oid tablespaceoid, const
123123
static void unlink_during_redo(Oid tablepace_oid_to_unlink);
124124
static void unlink_without_redo(Oid tablespace_oid_to_unlink);
125125

126+
static bool
127+
TablespaceLockTuple(Oid tablespace_oid, LOCKMODE lockmode, bool wait)
128+
{
129+
bool ok = true;
130+
131+
Assert(OidIsValid(tablespace_oid));
132+
Assert(tablespace_oid != GLOBALTABLESPACE_OID);
133+
Assert(tablespace_oid != DEFAULTTABLESPACE_OID);
134+
135+
if (wait)
136+
LockSharedObject(TableSpaceRelationId, tablespace_oid, 0, lockmode);
137+
else
138+
ok = ConditionalLockSharedObject(TableSpaceRelationId, tablespace_oid,
139+
0, lockmode);
140+
141+
return ok;
142+
}
143+
126144
/*
127145
* Each database using a table space is isolated into its own name space
128146
* by a subdirectory named for the database OID. On first creation of an
@@ -156,6 +174,9 @@ TablespaceCreateDbspace(Oid spcNode, Oid dbNode, bool isRedo)
156174
Assert(OidIsValid(spcNode));
157175
Assert(OidIsValid(dbNode));
158176

177+
if (spcNode != DEFAULTTABLESPACE_OID && !isRedo)
178+
TablespaceLockTuple(spcNode, AccessShareLock, true);
179+
159180
dir = GetDatabasePath(dbNode, spcNode);
160181

161182
if (stat(dir, &st) < 0)
@@ -720,6 +741,13 @@ DropTableSpace(DropTableSpaceStmt *stmt)
720741
/* DROP hook for the tablespace being removed */
721742
InvokeObjectDropHook(TableSpaceRelationId, tablespaceoid, 0);
722743

744+
if (!TablespaceLockTuple(tablespaceoid, AccessExclusiveLock, false))
745+
ereport(ERROR,
746+
(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
747+
errmsg("could not lock tablespace \"%s\"",
748+
tablespacename)));
749+
SIMPLE_FAULT_INJECTOR("drop_tablespace_after_acquire_lock");
750+
723751
/*
724752
* Remove the pg_tablespace tuple (this will roll back if we fail below)
725753
*/
@@ -795,7 +823,6 @@ DropTableSpace(DropTableSpaceStmt *stmt)
795823

796824
/* We keep the lock on pg_tablespace until commit */
797825
table_close(rel, NoLock);
798-
SIMPLE_FAULT_INJECTOR("AfterTablespaceCreateLockRelease");
799826

800827
/*
801828
* If we are the QD, dispatch this DROP command to all the QEs

src/backend/storage/lmgr/lmgr.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,27 @@ LockSharedObject(Oid classid, Oid objid, uint16 objsubid,
11531153
AcceptInvalidationMessages();
11541154
}
11551155

1156+
/*
1157+
* ConditionalLockSharedObject
1158+
*
1159+
* As above, but only lock if we can get the lock without blocking.
1160+
* Returns true iff the lock was acquired.
1161+
*/
1162+
bool
1163+
ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid,
1164+
LOCKMODE lockmode)
1165+
{
1166+
LOCKTAG tag;
1167+
1168+
SET_LOCKTAG_OBJECT(tag,
1169+
InvalidOid,
1170+
classid,
1171+
objid,
1172+
objsubid);
1173+
1174+
return (LockAcquire(&tag, lockmode, false, true) != LOCKACQUIRE_NOT_AVAIL);
1175+
}
1176+
11561177
/*
11571178
* UnlockSharedObject
11581179
*/

src/include/storage/lmgr.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ extern void UnlockDatabaseObject(Oid classid, Oid objid, uint16 objsubid,
103103
/* Lock a shared-across-databases object (other than a relation) */
104104
extern void LockSharedObject(Oid classid, Oid objid, uint16 objsubid,
105105
LOCKMODE lockmode);
106+
extern bool ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid,
107+
LOCKMODE lockmode);
106108
extern void UnlockSharedObject(Oid classid, Oid objid, uint16 objsubid,
107109
LOCKMODE lockmode);
108110

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
-- While a tablespace is being dropped, if any table is created
2-
-- in the same tablespace, the data of that table should not be deleted
2+
-- in the same tablespace, only one query can be successful.
3+
-- The behavior guarantees that the table will never use a
4+
-- dropped or invalid tablespace.
5+
6+
-- start_matchsubs
7+
-- m/ERROR: could not create directory "pg_tblspc.*: No such file or directory/
8+
-- s/ERROR: could not create directory "pg_tblspc.*: No such file or directory/ERROR: could not create directory "pg_tblspc\/XXXX": No such file or directory/
9+
-- end_matchsubs
310

411
-- create a tablespace directory
512
!\retcode rm -rf /tmp/concurrent_tblspace;
@@ -16,51 +23,112 @@
1623
CREATE TABLESPACE concurrent_tblspace LOCATION '/tmp/concurrent_tblspace';
1724
CREATE
1825

19-
-- suspend execution after TablespaceCreateLock is released
20-
SELECT gp_inject_fault('AfterTablespaceCreateLockRelease', 'suspend', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
26+
-- test 1:
27+
-- when creating a table using a tablespace, after the tuple of tablespace
28+
-- is locked, the tablespace is not allowed to drop
29+
2: begin;
30+
BEGIN
31+
2: CREATE TABLE t_in_tablespace(a int, b int) TABLESPACE concurrent_tblspace;
32+
CREATE
33+
34+
-- drop tablespace will fail: can't acuqire the lock
35+
DROP TABLESPACE concurrent_tblspace;
36+
ERROR: could not lock tablespace "concurrent_tblspace"
37+
2: rollback;
38+
ROLLBACK
39+
40+
-- test 2:
41+
-- if DROP TABLESPACE acquires lock first and rollback, the blocking CREATE
42+
-- TABLE will be successful.
43+
44+
-- suspend execution after tablespace lock is acquired
45+
SELECT gp_inject_fault('drop_tablespace_after_acquire_lock', 'suspend', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
2146
gp_inject_fault
2247
-----------------
2348
Success:
2449
Success:
2550
Success:
2651
(3 rows)
27-
1&:DROP TABLESPACE concurrent_tblspace; <waiting ...>
52+
1&: DROP TABLESPACE concurrent_tblspace; <waiting ...>
2853

2954
-- wait for the fault to be triggered
30-
SELECT gp_wait_until_triggered_fault('AfterTablespaceCreateLockRelease', 1, dbid) from gp_segment_configuration where content <> -1 and role='p';
55+
SELECT gp_wait_until_triggered_fault('drop_tablespace_after_acquire_lock', 1, dbid) from gp_segment_configuration where content <> -1 and role='p';
3156
gp_wait_until_triggered_fault
3257
-------------------------------
3358
Success:
3459
Success:
3560
Success:
3661
(3 rows)
3762

38-
-- create a table in the same tablespace which is being dropped via a concurrent session
39-
CREATE TABLE drop_tablespace_tbl(a int, b int) TABLESPACE concurrent_tblspace DISTRIBUTED BY (a);
63+
2&: CREATE TABLE t_in_tablespace(a int, b int) TABLESPACE concurrent_tblspace; <waiting ...>
64+
-- inject an error to ensure that the above DROP command will rollback
65+
SELECT gp_inject_fault('after_xlog_tblspc_drop', 'error', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
66+
gp_inject_fault
67+
-----------------
68+
Success:
69+
Success:
70+
Success:
71+
(3 rows)
72+
SELECT gp_inject_fault('drop_tablespace_after_acquire_lock', 'reset', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
73+
gp_inject_fault
74+
-----------------
75+
Success:
76+
Success:
77+
Success:
78+
(3 rows)
79+
-- fail
80+
1<: <... completed>
81+
ERROR: fault triggered, fault name:'after_xlog_tblspc_drop' fault type:'error'
82+
-- success
83+
2<: <... completed>
4084
CREATE
41-
INSERT INTO drop_tablespace_tbl SELECT i, i FROM generate_series(1,100)i;
42-
INSERT 100
43-
-- reset the fault, drop tablespace command will not delete the data files on the tablespace
44-
SELECT gp_inject_fault('AfterTablespaceCreateLockRelease', 'reset', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
85+
-- drop the above table, so the tablespace is empty.
86+
2: DROP TABLE t_in_tablespace;
87+
DROP
88+
SELECT gp_inject_fault('after_xlog_tblspc_drop', 'reset', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
89+
gp_inject_fault
90+
-----------------
91+
Success:
92+
Success:
93+
Success:
94+
(3 rows)
95+
96+
-- test 3:
97+
-- if DROP TABLESPACE acquires lock first and going to drop, any CREATE TABLE
98+
-- will fail
99+
100+
-- suspend execution after tablespace lock is acquired
101+
SELECT gp_inject_fault('drop_tablespace_after_acquire_lock', 'suspend', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
102+
gp_inject_fault
103+
-----------------
104+
Success:
105+
Success:
106+
Success:
107+
(3 rows)
108+
1&: DROP TABLESPACE concurrent_tblspace; <waiting ...>
109+
110+
-- wait for the fault to be triggered
111+
SELECT gp_wait_until_triggered_fault('drop_tablespace_after_acquire_lock', 1, dbid) from gp_segment_configuration where content <> -1 and role='p';
112+
gp_wait_until_triggered_fault
113+
-------------------------------
114+
Success:
115+
Success:
116+
Success:
117+
(3 rows)
118+
119+
-- create a table in the same tablespace which is being dropped via a concurrent session
120+
2&:CREATE TABLE drop_tablespace_tbl(a int, b int) TABLESPACE concurrent_tblspace DISTRIBUTED BY (a); <waiting ...>
121+
-- reset the fault, drop tablespace command will continue
122+
SELECT gp_inject_fault('drop_tablespace_after_acquire_lock', 'reset', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
45123
gp_inject_fault
46124
-----------------
47125
Success:
48126
Success:
49127
Success:
50128
(3 rows)
129+
-- success
51130
1<: <... completed>
52131
DROP
53-
-- check data exists
54-
SELECT count(*) FROM drop_tablespace_tbl;
55-
count
56-
-------
57-
100
58-
(1 row)
59-
-- move to another tablespace and check the data.
60-
ALTER TABLE drop_tablespace_tbl SET TABLESPACE pg_default;
61-
ALTER
62-
SELECT count(*) FROM drop_tablespace_tbl;
63-
count
64-
-------
65-
100
66-
(1 row)
132+
-- fail
133+
2<: <... completed>
134+
ERROR: could not create directory "pg_tblspc/33175/GPDB_1_302501601/32799": No such file or directory
Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,70 @@
11
-- While a tablespace is being dropped, if any table is created
2-
-- in the same tablespace, the data of that table should not be deleted
2+
-- in the same tablespace, only one query can be successful.
3+
-- The behavior guarantees that the table will never use a
4+
-- dropped or invalid tablespace.
5+
6+
-- start_matchsubs
7+
-- m/ERROR: could not create directory "pg_tblspc.*: No such file or directory/
8+
-- s/ERROR: could not create directory "pg_tblspc.*: No such file or directory/ERROR: could not create directory "pg_tblspc\/XXXX": No such file or directory/
9+
-- end_matchsubs
310

411
-- create a tablespace directory
512
!\retcode rm -rf /tmp/concurrent_tblspace;
613
!\retcode mkdir -p /tmp/concurrent_tblspace;
714

815
CREATE TABLESPACE concurrent_tblspace LOCATION '/tmp/concurrent_tblspace';
916

10-
-- suspend execution after TablespaceCreateLock is released
11-
SELECT gp_inject_fault('AfterTablespaceCreateLockRelease', 'suspend', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
12-
1&:DROP TABLESPACE concurrent_tblspace;
17+
-- test 1:
18+
-- when creating a table using a tablespace, after the tuple of tablespace
19+
-- is locked, the tablespace is not allowed to drop
20+
2: begin;
21+
2: CREATE TABLE t_in_tablespace(a int, b int) TABLESPACE concurrent_tblspace;
22+
23+
-- drop tablespace will fail: can't acuqire the lock
24+
DROP TABLESPACE concurrent_tblspace;
25+
2: rollback;
26+
27+
-- test 2:
28+
-- if DROP TABLESPACE acquires lock first and rollback, the blocking CREATE
29+
-- TABLE will be successful.
30+
31+
-- suspend execution after tablespace lock is acquired
32+
SELECT gp_inject_fault('drop_tablespace_after_acquire_lock', 'suspend', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
33+
1&: DROP TABLESPACE concurrent_tblspace;
34+
35+
-- wait for the fault to be triggered
36+
SELECT gp_wait_until_triggered_fault('drop_tablespace_after_acquire_lock', 1, dbid)
37+
from gp_segment_configuration where content <> -1 and role='p';
38+
39+
2&: CREATE TABLE t_in_tablespace(a int, b int) TABLESPACE concurrent_tblspace;
40+
-- inject an error to ensure that the above DROP command will rollback
41+
SELECT gp_inject_fault('after_xlog_tblspc_drop', 'error', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
42+
SELECT gp_inject_fault('drop_tablespace_after_acquire_lock', 'reset', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
43+
-- fail
44+
1<:
45+
-- success
46+
2<:
47+
-- drop the above table, so the tablespace is empty.
48+
2: DROP TABLE t_in_tablespace;
49+
SELECT gp_inject_fault('after_xlog_tblspc_drop', 'reset', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
50+
51+
-- test 3:
52+
-- if DROP TABLESPACE acquires lock first and going to drop, any CREATE TABLE
53+
-- will fail
54+
55+
-- suspend execution after tablespace lock is acquired
56+
SELECT gp_inject_fault('drop_tablespace_after_acquire_lock', 'suspend', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
57+
1&: DROP TABLESPACE concurrent_tblspace;
1358

1459
-- wait for the fault to be triggered
15-
SELECT gp_wait_until_triggered_fault('AfterTablespaceCreateLockRelease', 1, dbid)
60+
SELECT gp_wait_until_triggered_fault('drop_tablespace_after_acquire_lock', 1, dbid)
1661
from gp_segment_configuration where content <> -1 and role='p';
1762

1863
-- create a table in the same tablespace which is being dropped via a concurrent session
19-
CREATE TABLE drop_tablespace_tbl(a int, b int) TABLESPACE concurrent_tblspace DISTRIBUTED BY (a);
20-
INSERT INTO drop_tablespace_tbl SELECT i, i FROM generate_series(1,100)i;
21-
-- reset the fault, drop tablespace command will not delete the data files on the tablespace
22-
SELECT gp_inject_fault('AfterTablespaceCreateLockRelease', 'reset', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
64+
2&:CREATE TABLE drop_tablespace_tbl(a int, b int) TABLESPACE concurrent_tblspace DISTRIBUTED BY (a);
65+
-- reset the fault, drop tablespace command will continue
66+
SELECT gp_inject_fault('drop_tablespace_after_acquire_lock', 'reset', dbid) FROM gp_segment_configuration WHERE content <> -1 and role='p';
67+
-- success
2368
1<:
24-
-- check data exists
25-
SELECT count(*) FROM drop_tablespace_tbl;
26-
-- move to another tablespace and check the data.
27-
ALTER TABLE drop_tablespace_tbl SET TABLESPACE pg_default;
28-
SELECT count(*) FROM drop_tablespace_tbl;
69+
-- fail
70+
2<:

0 commit comments

Comments
 (0)