summaryrefslogtreecommitdiffstats
path: root/lib/tdb/test/run-fcntl-deadlock.c
blob: 0a328afaa508d1c4e6b77c19e124ebe1eca43eb5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#include "../common/tdb_private.h"
#include "../common/io.c"
#include "../common/tdb.c"
#include "../common/lock.c"
#include "../common/freelist.c"
#include "../common/traverse.c"
#include "../common/transaction.c"
#include "../common/error.c"
#include "../common/open.c"
#include "../common/check.c"
#include "../common/hash.c"
#include "../common/mutex.c"
#include "replace.h"
#include "system/filesys.h"
#include "system/time.h"
#include <errno.h>
#include "tap-interface.h"

/*
 * This tests the low level locking requirement
 * for the allrecord lock/prepare_commit and traverse_read interaction.
 *
 * The pattern with the traverse_read and prepare_commit interaction is
 * the following:
 *
 * 1. transaction_start got the allrecord lock with F_RDLCK.
 *
 * 2. the traverse_read code walks the database in a sequence like this
 * (per chain):
 *    2.1  chainlock(chainX, F_RDLCK)
 *    2.2  recordlock(chainX.record1, F_RDLCK)
 *    2.3  chainunlock(chainX, F_RDLCK)
 *    2.4  callback(chainX.record1)
 *    2.5  chainlock(chainX, F_RDLCK)
 *    2.6  recordunlock(chainX.record1, F_RDLCK)
 *    2.7  recordlock(chainX.record2, F_RDLCK)
 *    2.8  chainunlock(chainX, F_RDLCK)
 *    2.9  callback(chainX.record2)
 *    2.10 chainlock(chainX, F_RDLCK)
 *    2.11 recordunlock(chainX.record2, F_RDLCK)
 *    2.12 chainunlock(chainX, F_RDLCK)
 *    2.13 goto next chain
 *
 *    So it has always one record locked in F_RDLCK mode and tries to
 *    get the 2nd one before it releases the first one.
 *
 * 3. prepare_commit tries to upgrade the allrecord lock to F_RWLCK
 *    If that happens at the time of 2.4, the operation of
 *    2.5 may deadlock with the allrecord lock upgrade.
 *    On Linux step 2.5 works in order to make some progress with the
 *    locking, but on solaris it might fail because the kernel
 *    wants to satisfy the 1st lock requester before the 2nd one.
 *
 * I think the first step is a standalone test that does this:
 *
 * process1: F_RDLCK for ofs=0 len=2
 * process2: F_RDLCK for ofs=0 len=1
 * process1: upgrade ofs=0 len=2 to F_RWLCK (in blocking mode)
 * process2: F_RDLCK for ofs=1 len=1
 * process2: unlock ofs=0 len=2
 * process1: should continue at that point
 *
 * Such a test follows here...
 */

static int raw_fcntl_lock(int fd, int rw, off_t off, off_t len, bool waitflag)
{
	struct flock fl;
	int cmd;
	fl.l_type = rw;
	fl.l_whence = SEEK_SET;
	fl.l_start = off;
	fl.l_len = len;
	fl.l_pid = 0;

	cmd = waitflag ? F_SETLKW : F_SETLK;

	return fcntl(fd, cmd, &fl);
}

static int raw_fcntl_unlock(int fd, off_t off, off_t len)
{
	struct flock fl;
	fl.l_type = F_UNLCK;
	fl.l_whence = SEEK_SET;
	fl.l_start = off;
	fl.l_len = len;
	fl.l_pid = 0;

	return fcntl(fd, F_SETLKW, &fl);
}


int pipe_r;
int pipe_w;
char buf[2];

static void expect_char(char c)
{
	read(pipe_r, buf, 1);
	if (*buf != c) {
		fail("We were expecting %c, but got %c", c, buf[0]);
	}
}

static void send_char(char c)
{
	write(pipe_w, &c, 1);
}


int main(int argc, char *argv[])
{
	int process;
	int fd;
	const char *filename = "run-fcntl-deadlock.lck";
	int pid;
	int pipes_1_2[2];
	int pipes_2_1[2];
	int ret;

	pipe(pipes_1_2);
	pipe(pipes_2_1);
	fd = open(filename, O_RDWR | O_CREAT, 0755);

	pid = fork();
	if (pid == 0) {
		pipe_r = pipes_1_2[0];
		pipe_w = pipes_2_1[1];
		process = 2;
		alarm(15);
	} else {
		pipe_r = pipes_2_1[0];
		pipe_w = pipes_1_2[1];
		process = 1;
		alarm(15);
	}

	/* a: process1: F_RDLCK for ofs=0 len=2 */
	if (process == 1) {
		ret = raw_fcntl_lock(fd, F_RDLCK, 0, 2, true);
		ok(ret == 0,
		   "process 1 lock ofs=0 len=2: %d - %s",
		   ret, strerror(errno));
		diag("process 1 took read lock on range 0,2");
		send_char('a');
	}

	/* process2: F_RDLCK for ofs=0 len=1 */
	if (process == 2) {
		expect_char('a');
		ret = raw_fcntl_lock(fd, F_RDLCK, 0, 1, true);
		ok(ret == 0,
		   "process 2 lock ofs=0 len=1: %d - %s",
		   ret, strerror(errno));;
		diag("process 2 took read lock on range 0,1");
		send_char('b');
	}

	/* process1: upgrade ofs=0 len=2 to F_RWLCK (in blocking mode) */
	if (process == 1) {
		expect_char('b');
		send_char('c');
		diag("process 1 starts upgrade on range 0,2");
		ret = raw_fcntl_lock(fd, F_WRLCK, 0, 2, true);
		ok(ret == 0,
		   "process 1 RW lock ofs=0 len=2: %d - %s",
		   ret, strerror(errno));
		diag("process 1 got read upgrade done");
		/* at this point process 1 is blocked on 2 releasing the
		   read lock */
	}

	/*
	 * process2: F_RDLCK for ofs=1 len=1
	 * process2: unlock ofs=0 len=2
	 */
	if (process == 2) {
		expect_char('c'); /* we know process 1 is *about* to lock */
		sleep(1);
		ret = raw_fcntl_lock(fd, F_RDLCK, 1, 1, true);
		ok(ret == 0,
		  "process 2 lock ofs=1 len=1: %d - %s",
		  ret, strerror(errno));
		diag("process 2 got read lock on 1,1\n");
		ret = raw_fcntl_unlock(fd, 0, 2);
		ok(ret == 0,
		  "process 2 unlock ofs=0 len=2: %d - %s",
		  ret, strerror(errno));
		diag("process 2 released read lock on 0,2\n");
		sleep(1);
		send_char('d');
	}

	if (process == 1) {
		expect_char('d');
	}

	diag("process %d has got to the end\n", process);

	return 0;
}