whistlepig 0.9.1 → 0.10
Sign up to get free protection for your applications and to get access to all the features.
- data/README +40 -12
- data/ext/whistlepig/extconf.rb +1 -1
- data/ext/whistlepig/index.c +201 -62
- data/ext/whistlepig/index.h +11 -2
- data/ext/whistlepig/lock.c +153 -0
- data/ext/whistlepig/lock.h +18 -0
- data/ext/whistlepig/mmap-obj.c +36 -20
- data/ext/whistlepig/mmap-obj.h +12 -7
- data/ext/whistlepig/search.c +7 -6
- data/ext/whistlepig/segment.c +97 -47
- data/ext/whistlepig/segment.h +19 -3
- data/ext/whistlepig/stringmap.c +61 -56
- data/ext/whistlepig/stringmap.h +7 -14
- data/ext/whistlepig/termhash.c +60 -62
- data/ext/whistlepig/termhash.h +4 -6
- data/ext/whistlepig/whistlepig.c +5 -1
- data/ext/whistlepig/whistlepig.h +1 -0
- metadata +29 -38
- data/ext/whistlepig/dump.c +0 -65
- data/ext/whistlepig/extconf.h +0 -3
- data/ext/whistlepig/test-segment.c +0 -404
- data/ext/whistlepig/test-stringmap.c +0 -82
- data/ext/whistlepig/test-stringpool.c +0 -67
- data/ext/whistlepig/test-termhash.c +0 -95
- data/ext/whistlepig/test-tokenizer.c +0 -55
- data/ext/whistlepig/test.h +0 -38
- data/ext/whistlepig/timer.h +0 -28
data/README
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
= Whistlepig
|
2
2
|
|
3
|
-
Whistlepig is a minimalist realtime full-text search index. Its goal is
|
4
|
-
as small and
|
5
|
-
and scalable to large corpora. If you want realtime
|
6
|
-
the frills, Whistlepig may be for you.
|
3
|
+
Whistlepig is a minimalist realtime full-text search index. Its goal is
|
4
|
+
to be as small and maintainable as possible, while still remaining
|
5
|
+
useful, performant and scalable to large corpora. If you want realtime
|
6
|
+
full-text search without the frills, Whistlepig may be for you.
|
7
7
|
|
8
8
|
Whistlepig is written in ANSI C99. It currently provides a C API and Ruby
|
9
9
|
bindings.
|
10
10
|
|
11
|
-
Latest version: 0.
|
11
|
+
Latest version: 0.10, released 2012-04-01.
|
12
12
|
Status: beta
|
13
13
|
News: http://all-thing.net/label/whistlepig/
|
14
14
|
Homepage: http://masanjin.net/whistlepig/
|
@@ -27,7 +27,7 @@ Roughly speaking, realtime search means:
|
|
27
27
|
reindexing or index merging;
|
28
28
|
- later documents are more important than earlier documents.
|
29
29
|
|
30
|
-
Whistlepig takes these principles
|
30
|
+
Whistlepig takes these principles at face value.
|
31
31
|
- It only returns documents in the reverse (LIFO) order to which they were
|
32
32
|
added, and performs no ranking, reordering, or scoring.
|
33
33
|
- It only supports incremental indexing. There is no notion of batch indexing
|
@@ -47,6 +47,17 @@ Features that Whistlepig does provide:
|
|
47
47
|
- Early query termination and resumable queries.
|
48
48
|
- A tiny, < 3 KLOC ANSI C99 implementation.
|
49
49
|
|
50
|
+
== Benchmarks
|
51
|
+
|
52
|
+
On my not-particularly-new Linux desktop, I can index 8.5 MB/s of text
|
53
|
+
data per process, including some minor parsing.
|
54
|
+
|
55
|
+
Index sizes are roughly 50% of the original corpus size, e.g. the 1.4gb
|
56
|
+
Enron email corpus (http://cs.cmu.edu/~enron/) is 753mb in the index.
|
57
|
+
|
58
|
+
Query performance is entirely dependent on the queries and the index
|
59
|
+
size. Run the benchmark-queries to see some examples.
|
60
|
+
|
50
61
|
== Synopsis (using Ruby bindings)
|
51
62
|
|
52
63
|
require 'rubygems'
|
@@ -83,11 +94,28 @@ Features that Whistlepig does provide:
|
|
83
94
|
q4 = Query.new "body", "subject:know hello"
|
84
95
|
results4 = index.search q4 # => [3]
|
85
96
|
|
86
|
-
==
|
97
|
+
== Concurrency
|
98
|
+
|
99
|
+
Whistlepig supports multi-process concurrency. Multiple reader and
|
100
|
+
writer processes can access the same index without mangling data.
|
101
|
+
|
102
|
+
Internally, Whistlepig uses pthread read-write locks to synchronize
|
103
|
+
readers and writers. This allows multiple concurrent readers but only a
|
104
|
+
single writer.
|
105
|
+
|
106
|
+
While this locking approach guarantees index correctness, it decreases
|
107
|
+
read and write performance when one or more writers exist. Systems with
|
108
|
+
high write loads may benefit from sharding documents across independent
|
109
|
+
indexes rather than sending everything to the same index.
|
110
|
+
|
111
|
+
== Design tradeoffs
|
112
|
+
|
113
|
+
I have generally erred on the side of maintainable code at the expense
|
114
|
+
of speed. Simpler implementations have been preferred over more complex,
|
115
|
+
faster versions. If you ever have to modify Whistlepig to suit your
|
116
|
+
needs, you will appreciate this.
|
87
117
|
|
88
|
-
|
89
|
-
built with multi-process access in mind. Per-segment single-writer,
|
90
|
-
multi-reader support is planned in the near future. Multi-writer support can be
|
91
|
-
accomplished via index striping and may be attempted in the distant future.
|
118
|
+
== Bug reports
|
92
119
|
|
93
|
-
Please
|
120
|
+
Please file bugs here: https://github.com/wmorgan/whistlepig/issues
|
121
|
+
Please send comments to: wmorgan-whistlepig-readme@masanjin.net.
|
data/ext/whistlepig/extconf.rb
CHANGED
data/ext/whistlepig/index.c
CHANGED
@@ -6,6 +6,7 @@
|
|
6
6
|
#include "whistlepig.h"
|
7
7
|
|
8
8
|
#define PATH_BUF_SIZE 4096
|
9
|
+
#define INDEX_VERSION 1
|
9
10
|
|
10
11
|
int wp_index_exists(const char* pathname_base) {
|
11
12
|
char buf[PATH_BUF_SIZE];
|
@@ -13,74 +14,129 @@ int wp_index_exists(const char* pathname_base) {
|
|
13
14
|
return wp_segment_exists(buf);
|
14
15
|
}
|
15
16
|
|
17
|
+
RAISING_STATIC(grab_writelock(wp_index* index)) {
|
18
|
+
index_info* ii = MMAP_OBJ(index->indexinfo, index_info);
|
19
|
+
RELAY_ERROR(wp_lock_grab(&ii->lock, WP_LOCK_WRITELOCK));
|
20
|
+
return NO_ERROR;
|
21
|
+
}
|
22
|
+
|
23
|
+
RAISING_STATIC(grab_readlock(wp_index* index)) {
|
24
|
+
index_info* ii = MMAP_OBJ(index->indexinfo, index_info);
|
25
|
+
RELAY_ERROR(wp_lock_grab(&ii->lock, WP_LOCK_READLOCK));
|
26
|
+
return NO_ERROR;
|
27
|
+
}
|
28
|
+
|
29
|
+
RAISING_STATIC(release_lock(wp_index* index)) {
|
30
|
+
index_info* ii = MMAP_OBJ(index->indexinfo, index_info);
|
31
|
+
RELAY_ERROR(wp_lock_release(&ii->lock));
|
32
|
+
return NO_ERROR;
|
33
|
+
}
|
34
|
+
|
35
|
+
|
36
|
+
RAISING_STATIC(index_info_init(index_info* ii, uint32_t index_version)) {
|
37
|
+
ii->index_version = index_version;
|
38
|
+
ii->num_segments = 0;
|
39
|
+
|
40
|
+
RELAY_ERROR(wp_lock_setup(&ii->lock));
|
41
|
+
return NO_ERROR;
|
42
|
+
}
|
43
|
+
|
44
|
+
RAISING_STATIC(index_info_validate(index_info* ii, uint32_t index_version)) {
|
45
|
+
if(ii->index_version != index_version) RAISE_ERROR("index has type %u; expecting type %u", ii->index_version, index_version);
|
46
|
+
return NO_ERROR;
|
47
|
+
}
|
48
|
+
|
16
49
|
wp_error* wp_index_create(wp_index** indexptr, const char* pathname_base) {
|
17
50
|
char buf[PATH_BUF_SIZE];
|
18
51
|
|
19
|
-
snprintf(buf, PATH_BUF_SIZE, "%s0", pathname_base);
|
20
|
-
if(wp_segment_exists(buf)) RAISE_ERROR("index with base path '%s' already exists", pathname_base);
|
21
|
-
|
22
52
|
wp_index* index = *indexptr = malloc(sizeof(wp_index));
|
53
|
+
snprintf(buf, PATH_BUF_SIZE, "%s.ii", pathname_base);
|
54
|
+
RELAY_ERROR(mmap_obj_create(&index->indexinfo, "wp/indexinfo", buf, sizeof(index_info)));
|
55
|
+
RELAY_ERROR(index_info_init(MMAP_OBJ(index->indexinfo, index_info), INDEX_VERSION));
|
56
|
+
|
23
57
|
index->pathname_base = pathname_base;
|
24
|
-
index->num_segments = 1;
|
25
58
|
index->sizeof_segments = 1;
|
26
59
|
index->open = 1;
|
27
60
|
index->segments = malloc(sizeof(wp_segment));
|
28
61
|
index->docid_offsets = malloc(sizeof(uint64_t));
|
29
62
|
|
63
|
+
snprintf(buf, PATH_BUF_SIZE, "%s0", pathname_base);
|
30
64
|
RELAY_ERROR(wp_segment_create(&index->segments[0], buf));
|
31
65
|
index->docid_offsets[0] = 0;
|
66
|
+
index->num_segments = 1;
|
67
|
+
|
68
|
+
index_info* ii = MMAP_OBJ(index->indexinfo, index_info);
|
69
|
+
ii->num_segments = 1;
|
32
70
|
|
33
71
|
return NO_ERROR;
|
34
72
|
}
|
35
73
|
|
36
|
-
|
74
|
+
// increases the index->segments array until we have enough
|
75
|
+
// space to represent index->num_segments
|
76
|
+
RAISING_STATIC(ensure_segment_pointer_fit(wp_index* index)) {
|
37
77
|
if(index->num_segments >= index->sizeof_segments) {
|
38
|
-
index->sizeof_segments
|
78
|
+
if(index->sizeof_segments == 0) index->sizeof_segments = 1; // lame
|
79
|
+
while(index->sizeof_segments < index->num_segments) index->sizeof_segments *= 2; // lame
|
39
80
|
index->segments = realloc(index->segments, sizeof(wp_segment) * index->sizeof_segments);
|
40
81
|
index->docid_offsets = realloc(index->docid_offsets, sizeof(uint64_t) * index->sizeof_segments);
|
41
82
|
if(index->segments == NULL) RAISE_ERROR("oom");
|
83
|
+
if(index->segments == NULL) RAISE_ERROR("oom");
|
42
84
|
}
|
43
85
|
|
44
86
|
return NO_ERROR;
|
45
87
|
}
|
46
88
|
|
47
|
-
|
89
|
+
// ensures that we know about all segments. should be wrapped
|
90
|
+
// in a global read mutex to prevent creation.
|
91
|
+
RAISING_STATIC(ensure_all_segments(wp_index* index)) {
|
48
92
|
char buf[PATH_BUF_SIZE];
|
49
|
-
snprintf(buf, PATH_BUF_SIZE, "%s0", pathname_base);
|
50
|
-
if(!wp_segment_exists(buf)) RAISE_ERROR("index with base path '%s' does not exist", pathname_base);
|
51
93
|
|
52
|
-
|
94
|
+
index_info* ii = MMAP_OBJ(index->indexinfo, index_info);
|
95
|
+
if(ii->num_segments < index->num_segments) RAISE_ERROR("invalid value for num_segments: %u vs %u", index->num_segments, ii->num_segments);
|
96
|
+
if(ii->num_segments == index->num_segments) return NO_ERROR;
|
53
97
|
|
54
|
-
|
55
|
-
index->num_segments
|
56
|
-
index->
|
57
|
-
index
|
58
|
-
index->segments = malloc(sizeof(wp_segment));
|
59
|
-
index->docid_offsets = malloc(sizeof(uint64_t));
|
98
|
+
// otherwise, we need to load some more segments
|
99
|
+
uint16_t old_num_segments = index->num_segments;
|
100
|
+
index->num_segments = ii->num_segments;
|
101
|
+
RELAY_ERROR(ensure_segment_pointer_fit(index));
|
60
102
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
103
|
+
for(uint16_t i = old_num_segments; i < index->num_segments; i++) {
|
104
|
+
snprintf(buf, PATH_BUF_SIZE, "%s%u", index->pathname_base, i);
|
105
|
+
DEBUG("trying to loading segment %u from %s", i, buf);
|
106
|
+
RELAY_ERROR(wp_segment_load(&index->segments[i], buf));
|
65
107
|
|
66
|
-
|
67
|
-
DEBUG("loading segment %s", buf);
|
68
|
-
RELAY_ERROR(wp_segment_load(&index->segments[index->num_segments], buf));
|
69
|
-
if(index->num_segments == 0)
|
70
|
-
index->docid_offsets[index->num_segments] = 0;
|
108
|
+
if(i == 0) index->docid_offsets[i] = 0;
|
71
109
|
else {
|
72
110
|
// segments return docids 1 through N, so the num_docs in a segment is
|
73
111
|
// also the max document id
|
74
|
-
|
75
|
-
index->docid_offsets[
|
112
|
+
segment_info* prevsi = MMAP_OBJ(index->segments[i - 1].seginfo, segment_info);
|
113
|
+
index->docid_offsets[i] = prevsi->num_docs + index->docid_offsets[i - 1];
|
76
114
|
}
|
77
|
-
|
78
|
-
index->num_segments++;
|
79
115
|
}
|
80
116
|
|
81
117
|
return NO_ERROR;
|
82
118
|
}
|
83
119
|
|
120
|
+
wp_error* wp_index_load(wp_index** indexptr, const char* pathname_base) {
|
121
|
+
char buf[PATH_BUF_SIZE];
|
122
|
+
|
123
|
+
wp_index* index = *indexptr = malloc(sizeof(wp_index));
|
124
|
+
snprintf(buf, PATH_BUF_SIZE, "%s.ii", pathname_base);
|
125
|
+
RELAY_ERROR(mmap_obj_load(&index->indexinfo, "wp/indexinfo", buf));
|
126
|
+
RELAY_ERROR(index_info_validate(MMAP_OBJ(index->indexinfo, index_info), INDEX_VERSION));
|
127
|
+
|
128
|
+
index->pathname_base = pathname_base;
|
129
|
+
index->open = 1;
|
130
|
+
index->num_segments = 0;
|
131
|
+
index->sizeof_segments = 0;
|
132
|
+
index->segments = NULL;
|
133
|
+
index->docid_offsets = NULL;
|
134
|
+
|
135
|
+
RELAY_ERROR(ensure_all_segments(index));
|
136
|
+
|
137
|
+
return NO_ERROR;
|
138
|
+
}
|
139
|
+
|
84
140
|
// we have two special values at our disposal to mark where we are in
|
85
141
|
// the sequence of segments
|
86
142
|
#define SEGMENT_UNINITIALIZED WP_MAX_SEGMENTS
|
@@ -96,12 +152,22 @@ wp_error* wp_index_setup_query(wp_index* index, wp_query* query) {
|
|
96
152
|
// can be called multiple times to resume
|
97
153
|
wp_error* wp_index_run_query(wp_index* index, wp_query* query, uint32_t max_num_results, uint32_t* num_results, uint64_t* results) {
|
98
154
|
*num_results = 0;
|
155
|
+
|
156
|
+
// make sure we have know about all segments (one could've been added by a writer)
|
157
|
+
RELAY_ERROR(grab_readlock(index));
|
158
|
+
RELAY_ERROR(ensure_all_segments(index));
|
159
|
+
RELAY_ERROR(release_lock(index));
|
160
|
+
|
99
161
|
if(index->num_segments == 0) return NO_ERROR;
|
100
162
|
|
101
163
|
if(query->segment_idx == SEGMENT_UNINITIALIZED) {
|
102
164
|
query->segment_idx = index->num_segments - 1;
|
103
165
|
DEBUG("setting up segment %u", query->segment_idx);
|
104
|
-
|
166
|
+
wp_segment* seg = &index->segments[query->segment_idx];
|
167
|
+
RELAY_ERROR(wp_segment_grab_readlock(seg));
|
168
|
+
RELAY_ERROR(wp_segment_reload(seg));
|
169
|
+
RELAY_ERROR(wp_search_init_search_state(query, seg));
|
170
|
+
RELAY_ERROR(wp_segment_release_lock(seg));
|
105
171
|
}
|
106
172
|
|
107
173
|
// at this point, we assume we're initialized and query->segment_idx is the index
|
@@ -112,7 +178,11 @@ wp_error* wp_index_run_query(wp_index* index, wp_query* query, uint32_t max_num_
|
|
112
178
|
search_result* segment_results = malloc(sizeof(search_result) * want_num_results);
|
113
179
|
|
114
180
|
DEBUG("searching segment %d", query->segment_idx);
|
115
|
-
|
181
|
+
wp_segment* seg = &index->segments[query->segment_idx];
|
182
|
+
RELAY_ERROR(wp_segment_grab_readlock(seg));
|
183
|
+
RELAY_ERROR(wp_segment_reload(seg));
|
184
|
+
RELAY_ERROR(wp_search_run_query_on_segment(query, seg, want_num_results, &got_num_results, segment_results));
|
185
|
+
RELAY_ERROR(wp_segment_release_lock(seg));
|
116
186
|
DEBUG("asked segment %d for %d results, got %d", query->segment_idx, want_num_results, got_num_results);
|
117
187
|
|
118
188
|
// extract the per-segment docids from the search results and adjust by
|
@@ -168,39 +238,73 @@ wp_error* wp_index_teardown_query(wp_index* index, wp_query* query) {
|
|
168
238
|
return NO_ERROR;
|
169
239
|
}
|
170
240
|
|
171
|
-
|
172
|
-
|
173
|
-
|
241
|
+
RAISING_STATIC(get_and_writelock_last_segment(wp_index* index, wp_entry* entry, wp_segment** returned_seg)) {
|
242
|
+
// assume we have a writelock on the index object here, so that no one can
|
243
|
+
// add segments while we're doing this stuff.
|
174
244
|
|
175
|
-
|
176
|
-
|
245
|
+
int success;
|
246
|
+
RELAY_ERROR(ensure_all_segments(index)); // make sure we know about all segments
|
247
|
+
wp_segment* seg = &index->segments[index->num_segments - 1]; // get last segment
|
248
|
+
RELAY_ERROR(wp_segment_grab_writelock(seg)); // grab the writelock
|
249
|
+
uint32_t postings_bytes; // calculate how much space we'll need to fit this entry in there
|
177
250
|
RELAY_ERROR(wp_entry_sizeof_postings_region(entry, seg, &postings_bytes));
|
178
251
|
RELAY_ERROR(wp_segment_ensure_fit(seg, postings_bytes, 0, &success));
|
179
252
|
|
180
|
-
// if
|
181
|
-
if(
|
182
|
-
|
183
|
-
|
184
|
-
snprintf(buf, PATH_BUF_SIZE, "%s%d", index->pathname_base, index->num_segments);
|
185
|
-
RELAY_ERROR(ensure_num_segments(index));
|
186
|
-
RELAY_ERROR(wp_segment_create(&index->segments[index->num_segments], buf));
|
187
|
-
index->num_segments++;
|
188
|
-
|
189
|
-
// set the docid_offset
|
190
|
-
postings_region* prevpr = MMAP_OBJ(index->segments[index->num_segments - 2].postings, postings_region);
|
191
|
-
index->docid_offsets[index->num_segments - 1] = prevpr->num_docs + index->docid_offsets[index->num_segments - 2];
|
192
|
-
|
193
|
-
seg = &index->segments[index->num_segments - 1];
|
194
|
-
DEBUG("loaded new segment %d at %p", index->num_segments - 1, &index->segments[index->num_segments - 1]);
|
195
|
-
|
196
|
-
RELAY_ERROR(wp_entry_sizeof_postings_region(entry, seg, &postings_bytes));
|
197
|
-
RELAY_ERROR(wp_segment_ensure_fit(seg, postings_bytes, 0, &success));
|
198
|
-
if(!success) RAISE_ERROR("can't fit new entry into fresh segment. that's crazy");
|
253
|
+
// if we can fit in there, then return it! (still locked)
|
254
|
+
if(success) {
|
255
|
+
*returned_seg = seg;
|
256
|
+
return NO_ERROR;
|
199
257
|
}
|
200
258
|
|
259
|
+
RAISE_ERROR("making new");
|
260
|
+
|
261
|
+
// otherwise, unlock it and let's make a new one
|
262
|
+
RELAY_ERROR(wp_segment_release_lock(seg));
|
263
|
+
|
264
|
+
char buf[PATH_BUF_SIZE];
|
265
|
+
DEBUG("segment %d is full, loading a new one", index->num_segments - 1);
|
266
|
+
snprintf(buf, PATH_BUF_SIZE, "%s%d", index->pathname_base, index->num_segments);
|
267
|
+
|
268
|
+
// increase the two counters
|
269
|
+
index_info* ii = MMAP_OBJ(index->indexinfo, index_info);
|
270
|
+
ii->num_segments++;
|
271
|
+
index->num_segments++;
|
272
|
+
|
273
|
+
// make sure we have a pointer for this guy
|
274
|
+
RELAY_ERROR(ensure_segment_pointer_fit(index));
|
275
|
+
|
276
|
+
// create the new segment
|
277
|
+
RELAY_ERROR(wp_segment_create(&index->segments[index->num_segments - 1], buf));
|
278
|
+
|
279
|
+
// set the docid_offset
|
280
|
+
segment_info* prevsi = MMAP_OBJ(index->segments[index->num_segments - 2].seginfo, segment_info);
|
281
|
+
index->docid_offsets[index->num_segments - 1] = prevsi->num_docs + index->docid_offsets[index->num_segments - 2];
|
282
|
+
|
283
|
+
seg = &index->segments[index->num_segments - 1];
|
284
|
+
DEBUG("loaded new segment %d at %p", index->num_segments - 1, seg);
|
285
|
+
|
286
|
+
RELAY_ERROR(wp_segment_grab_writelock(seg)); // lock it
|
287
|
+
RELAY_ERROR(wp_entry_sizeof_postings_region(entry, seg, &postings_bytes));
|
288
|
+
RELAY_ERROR(wp_segment_ensure_fit(seg, postings_bytes, 0, &success));
|
289
|
+
if(!success) RAISE_ERROR("can't fit new entry into fresh segment. that's crazy");
|
290
|
+
|
291
|
+
*returned_seg = seg;
|
292
|
+
return NO_ERROR;
|
293
|
+
}
|
294
|
+
|
295
|
+
wp_error* wp_index_add_entry(wp_index* index, wp_entry* entry, uint64_t* doc_id) {
|
296
|
+
wp_segment* seg = NULL;
|
201
297
|
docid_t seg_doc_id;
|
298
|
+
|
299
|
+
// interleaving lock access -- potential for deadlock is high. :(
|
300
|
+
RELAY_ERROR(grab_writelock(index)); // grab full-index lock
|
301
|
+
RELAY_ERROR(get_and_writelock_last_segment(index, entry, &seg));
|
302
|
+
RELAY_ERROR(release_lock(index)); // release full-index lock
|
303
|
+
|
304
|
+
RELAY_ERROR(wp_segment_reload(seg));
|
202
305
|
RELAY_ERROR(wp_segment_grab_docid(seg, &seg_doc_id));
|
203
306
|
RELAY_ERROR(wp_entry_write_to_segment(entry, seg, seg_doc_id));
|
307
|
+
RELAY_ERROR(wp_segment_release_lock(seg));
|
204
308
|
*doc_id = seg_doc_id + index->docid_offsets[index->num_segments - 1];
|
205
309
|
|
206
310
|
return NO_ERROR;
|
@@ -226,7 +330,11 @@ wp_error* wp_index_dumpinfo(wp_index* index, FILE* stream) {
|
|
226
330
|
fprintf(stream, "index has %d segments\n", index->num_segments);
|
227
331
|
for(int i = 0; i < index->num_segments; i++) {
|
228
332
|
fprintf(stream, "\nsegment %d:\n", i);
|
229
|
-
|
333
|
+
wp_segment* seg = &index->segments[i];
|
334
|
+
RELAY_ERROR(wp_segment_grab_readlock(seg));
|
335
|
+
RELAY_ERROR(wp_segment_reload(seg));
|
336
|
+
RELAY_ERROR(wp_segment_dumpinfo(seg, stream));
|
337
|
+
RELAY_ERROR(wp_segment_release_lock(seg));
|
230
338
|
}
|
231
339
|
|
232
340
|
return NO_ERROR;
|
@@ -246,16 +354,28 @@ wp_error* wp_index_delete(const char* pathname_base) {
|
|
246
354
|
else break;
|
247
355
|
}
|
248
356
|
|
357
|
+
snprintf(buf, PATH_BUF_SIZE, "%s.ii", pathname_base);
|
358
|
+
unlink(buf);
|
359
|
+
|
249
360
|
return NO_ERROR;
|
250
361
|
}
|
251
362
|
|
252
363
|
wp_error* wp_index_add_label(wp_index* index, const char* label, uint64_t doc_id) {
|
253
364
|
int found = 0;
|
254
365
|
|
366
|
+
RELAY_ERROR(grab_writelock(index));
|
367
|
+
RELAY_ERROR(ensure_all_segments(index));
|
368
|
+
RELAY_ERROR(release_lock(index));
|
369
|
+
|
255
370
|
for(uint32_t i = index->num_segments; i > 0; i--) {
|
256
371
|
if(doc_id > index->docid_offsets[i - 1]) {
|
372
|
+
wp_segment* seg = &index->segments[i - 1];
|
373
|
+
|
257
374
|
DEBUG("found doc %llu in segment %u", doc_id, i - 1);
|
258
|
-
RELAY_ERROR(
|
375
|
+
RELAY_ERROR(wp_segment_grab_writelock(seg));
|
376
|
+
RELAY_ERROR(wp_segment_reload(seg));
|
377
|
+
RELAY_ERROR(wp_segment_add_label(seg, label, (docid_t)(doc_id - index->docid_offsets[i - 1])));
|
378
|
+
RELAY_ERROR(wp_segment_release_lock(seg));
|
259
379
|
found = 1;
|
260
380
|
break;
|
261
381
|
}
|
@@ -270,10 +390,19 @@ wp_error* wp_index_add_label(wp_index* index, const char* label, uint64_t doc_id
|
|
270
390
|
wp_error* wp_index_remove_label(wp_index* index, const char* label, uint64_t doc_id) {
|
271
391
|
int found = 0;
|
272
392
|
|
393
|
+
RELAY_ERROR(grab_writelock(index));
|
394
|
+
RELAY_ERROR(ensure_all_segments(index));
|
395
|
+
RELAY_ERROR(release_lock(index));
|
396
|
+
|
273
397
|
for(uint32_t i = index->num_segments; i > 0; i--) {
|
274
398
|
if(doc_id > index->docid_offsets[i - 1]) {
|
399
|
+
wp_segment* seg = &index->segments[i - 1];
|
400
|
+
|
275
401
|
DEBUG("found doc %llu in segment %u", doc_id, i - 1);
|
276
|
-
RELAY_ERROR(
|
402
|
+
RELAY_ERROR(wp_segment_grab_writelock(seg));
|
403
|
+
RELAY_ERROR(wp_segment_reload(seg));
|
404
|
+
RELAY_ERROR(wp_segment_remove_label(seg, label, (docid_t)(doc_id - index->docid_offsets[i - 1])));
|
405
|
+
RELAY_ERROR(wp_segment_release_lock(seg));
|
277
406
|
found = 1;
|
278
407
|
break;
|
279
408
|
}
|
@@ -285,13 +414,23 @@ wp_error* wp_index_remove_label(wp_index* index, const char* label, uint64_t doc
|
|
285
414
|
return NO_ERROR;
|
286
415
|
}
|
287
416
|
|
288
|
-
|
289
|
-
|
417
|
+
wp_error* wp_index_num_docs(wp_index* index, uint64_t* num_docs) {
|
418
|
+
*num_docs = 0;
|
419
|
+
|
420
|
+
RELAY_ERROR(grab_readlock(index));
|
421
|
+
RELAY_ERROR(ensure_all_segments(index));
|
422
|
+
RELAY_ERROR(release_lock(index));
|
290
423
|
|
291
424
|
// TODO check for overflow or some shit
|
292
|
-
for(uint32_t i = index->num_segments; i > 0; i--)
|
425
|
+
for(uint32_t i = index->num_segments; i > 0; i--) {
|
426
|
+
wp_segment* seg = &index->segments[i - 1];
|
427
|
+
RELAY_ERROR(wp_segment_grab_readlock(seg));
|
428
|
+
RELAY_ERROR(wp_segment_reload(seg));
|
429
|
+
*num_docs += wp_segment_num_docs(seg);
|
430
|
+
RELAY_ERROR(wp_segment_release_lock(seg));
|
431
|
+
}
|
293
432
|
|
294
|
-
return
|
433
|
+
return NO_ERROR;
|
295
434
|
}
|
296
435
|
|
297
436
|
// insane. but i'm putting this here. not defined in c99. don't want to make a
|
data/ext/whistlepig/index.h
CHANGED
@@ -9,6 +9,8 @@
|
|
9
9
|
// essentially relays commands to the appropriate ones, creating new segments
|
10
10
|
// as needed.
|
11
11
|
|
12
|
+
#include <pthread.h>
|
13
|
+
|
12
14
|
#include "defaults.h"
|
13
15
|
#include "segment.h"
|
14
16
|
#include "error.h"
|
@@ -16,13 +18,20 @@
|
|
16
18
|
|
17
19
|
#define WP_MAX_SEGMENTS 65534 // max value of wp_search_query->segment_idx - 2 because we need two special numbers
|
18
20
|
|
21
|
+
typedef struct index_info {
|
22
|
+
uint32_t index_version;
|
23
|
+
uint32_t num_segments;
|
24
|
+
pthread_rwlock_t lock;
|
25
|
+
} index_info;
|
26
|
+
|
19
27
|
typedef struct wp_index {
|
20
28
|
const char* pathname_base;
|
21
29
|
uint16_t num_segments;
|
22
30
|
uint16_t sizeof_segments;
|
23
31
|
uint64_t* docid_offsets;
|
24
|
-
|
32
|
+
wp_segment* segments;
|
25
33
|
uint8_t open;
|
34
|
+
mmap_obj indexinfo;
|
26
35
|
} wp_index;
|
27
36
|
|
28
37
|
// API methods
|
@@ -45,7 +54,7 @@ wp_error* wp_index_unload(wp_index* index) RAISES_ERROR;
|
|
45
54
|
wp_error* wp_index_free(wp_index* index) RAISES_ERROR;
|
46
55
|
|
47
56
|
// public: returns the number of documents in the index.
|
48
|
-
|
57
|
+
wp_error* wp_index_num_docs(wp_index* index, uint64_t* num_docs) RAISES_ERROR;
|
49
58
|
|
50
59
|
// public: initializes a query for use on the index. must be called before
|
51
60
|
// run_query
|
@@ -0,0 +1,153 @@
|
|
1
|
+
#include <unistd.h>
|
2
|
+
#include <pthread.h>
|
3
|
+
#include "whistlepig.h"
|
4
|
+
|
5
|
+
wp_error* wp_lock_setup(pthread_rwlock_t* lock) {
|
6
|
+
pthread_rwlockattr_t attr;
|
7
|
+
if(pthread_rwlockattr_init(&attr) != 0) RAISE_SYSERROR("cannot initialize pthreads rwlock attr");
|
8
|
+
if(pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) != 0) RAISE_SYSERROR("cannot set pthreads rwlockattr to PTHREAD_PROCESS_SHARED");
|
9
|
+
if(pthread_rwlock_init(lock, &attr) != 0) RAISE_SYSERROR("cannot initialize pthreads rwlock");
|
10
|
+
if(pthread_rwlockattr_destroy(&attr) != 0) RAISE_SYSERROR("cannot destroy pthreads rwlock attr");
|
11
|
+
|
12
|
+
return NO_ERROR;
|
13
|
+
}
|
14
|
+
|
15
|
+
/*
|
16
|
+
|
17
|
+
alernative implementation that uses rdlock. doesn't allow us to detect
|
18
|
+
and break stale locks, but gives us a good sense of what the timing should
|
19
|
+
be like.
|
20
|
+
|
21
|
+
wp_error* wp_segment_grab_lock2(wp_segment* seg, int lock_type) {
|
22
|
+
segment_info* si = MMAP_OBJ(seg->seginfo, segment_info);
|
23
|
+
const char* lock_name = (lock_type == WP_LOCK_READLOCK ? "read" : "write");
|
24
|
+
DEBUG("grabbing %slock for segment %p", lock_name, seg);
|
25
|
+
|
26
|
+
struct timespec start, end;
|
27
|
+
clock_gettime(CLOCK_MONOTONIC, &start);
|
28
|
+
|
29
|
+
int ret = 0;
|
30
|
+
switch(lock_type) {
|
31
|
+
case WP_LOCK_READLOCK: ret = pthread_rwlock_rdlock(&si->lock); break;
|
32
|
+
case WP_LOCK_WRITELOCK: ret = pthread_rwlock_wrlock(&si->lock); break;
|
33
|
+
}
|
34
|
+
|
35
|
+
if(ret != 0) RAISE_SYSERROR("grabbing %slock", lock_name);
|
36
|
+
|
37
|
+
clock_gettime(CLOCK_MONOTONIC, &end);
|
38
|
+
|
39
|
+
|
40
|
+
uint64_t diff_in_ns = ((end.tv_sec * 1000000000) + end.tv_nsec) -
|
41
|
+
((start.tv_sec * 1000000000) + start.tv_nsec);
|
42
|
+
|
43
|
+
uint32_t total_delay_ms = diff_in_ns / 1000000;
|
44
|
+
if(total_delay_ms > 0) printf("XXX acquired %slock for segment %p after %ums\n", lock_name, seg, total_delay_ms);
|
45
|
+
|
46
|
+
return NO_ERROR;
|
47
|
+
}
|
48
|
+
*/
|
49
|
+
|
50
|
+
// we will wait this many milliseconds before assuming the lock
|
51
|
+
// is stale and breaking it.
|
52
|
+
#define LOCK_STALE_TIME_MS 2500
|
53
|
+
|
54
|
+
/* here's the best implementation i can find, empirically, of being
|
55
|
+
able to grab pthread read and write locks, while still being able
|
56
|
+
to detect stale locks and repair them.
|
57
|
+
|
58
|
+
it involves a busyloop, which is lame.
|
59
|
+
*/
|
60
|
+
wp_error* wp_lock_grab(pthread_rwlock_t* lock, int lock_type) {
|
61
|
+
const char* lock_name = (lock_type == WP_LOCK_READLOCK ? "read" : "write");
|
62
|
+
DEBUG("grabbing %slock at %p", lock_name, lock);
|
63
|
+
|
64
|
+
unsigned int delay_ms = 1;
|
65
|
+
uint32_t total_delay_ms = 0;
|
66
|
+
|
67
|
+
while(1) {
|
68
|
+
int ret = 0;
|
69
|
+
|
70
|
+
switch(lock_type) {
|
71
|
+
case WP_LOCK_READLOCK: ret = pthread_rwlock_tryrdlock(lock); break;
|
72
|
+
case WP_LOCK_WRITELOCK: ret = pthread_rwlock_trywrlock(lock); break;
|
73
|
+
default: RAISE_ERROR("invalid lock type");
|
74
|
+
}
|
75
|
+
|
76
|
+
if(ret == 0) break; // acquired!
|
77
|
+
// we get EAGAINs here if the writer died before closing the lock.
|
78
|
+
if((ret != EBUSY) && (ret != EAGAIN)) RAISE_SYSERROR("acquiring %slock", lock_name);
|
79
|
+
|
80
|
+
if(total_delay_ms >= LOCK_STALE_TIME_MS) {
|
81
|
+
//RAISE_ERROR("timeout acquiring %slock: %ums", lock_name, total_delay_ms);
|
82
|
+
DEBUG("assuming lock is stale and breaking it!");
|
83
|
+
RELAY_ERROR(wp_lock_setup(lock));
|
84
|
+
}
|
85
|
+
if(delay_ms > 1000) sleep(delay_ms / 1000);
|
86
|
+
usleep(1000 * (delay_ms % 1000));
|
87
|
+
total_delay_ms += delay_ms;
|
88
|
+
}
|
89
|
+
|
90
|
+
if(total_delay_ms > 0) DEBUG(":( acquired %slock for after %ums\n", lock_name, total_delay_ms);
|
91
|
+
return NO_ERROR;
|
92
|
+
}
|
93
|
+
|
94
|
+
/* an alternative implementation that uses the _timed pthread operations.
|
95
|
+
although this should be the best version, i had many problems with it. the
|
96
|
+
timeout didn't seem to ever trigger. i would also see an EINVAL whenever a
|
97
|
+
writer had a readlock. in the case of a stale lock, i would just get EINVALS
|
98
|
+
forever rather than a proper ETIMEDOUT.
|
99
|
+
|
100
|
+
since using this would require implementing my own stale lock detection
|
101
|
+
anyways, so i'm just going to use the simpler version above instead.
|
102
|
+
*/
|
103
|
+
/*
|
104
|
+
wp_error* wp_segment_grab_lock3(wp_segment* seg, int lock_type) {
|
105
|
+
segment_info* si = MMAP_OBJ(seg->seginfo, segment_info);
|
106
|
+
const char* lock_name = (lock_type == WP_LOCK_READLOCK ? "read" : "write");
|
107
|
+
DEBUG("grabbing %slock for segment %p", lock_name, seg);
|
108
|
+
|
109
|
+
struct timespec timeout;
|
110
|
+
timeout.tv_sec = 3;//LOCK_STALE_TIME_MS / 1000;
|
111
|
+
timeout.tv_nsec = 0;//(LOCK_STALE_TIME_MS % 1000) * 1000000;
|
112
|
+
|
113
|
+
struct timeval startt, endt;
|
114
|
+
gettimeofday(&startt, NULL);
|
115
|
+
|
116
|
+
int acquired = 0;
|
117
|
+
while(!acquired) {
|
118
|
+
int ret = 0;
|
119
|
+
|
120
|
+
switch(lock_type) {
|
121
|
+
case WP_LOCK_READLOCK: ret = pthread_rwlock_timedrdlock(&si->lock, &timeout); break;
|
122
|
+
case WP_LOCK_WRITELOCK: ret = pthread_rwlock_timedwrlock(&si->lock, &timeout); break;
|
123
|
+
default: RAISE_ERROR("invalid lock type");
|
124
|
+
}
|
125
|
+
|
126
|
+
switch(ret) {
|
127
|
+
case 0: acquired = 1; break;
|
128
|
+
case ETIMEDOUT:
|
129
|
+
DEBUG("assuming lock is stale and breaking it!");
|
130
|
+
RELAY_ERROR(setup_lock(&si->lock));
|
131
|
+
break;
|
132
|
+
case EAGAIN:
|
133
|
+
// despite the documentation, this seems to happen every time we request a readlock and
|
134
|
+
// the lock is already held by the writer. so we will just busyloop here. this happens
|
135
|
+
// fairly frequently, so this is lame.
|
136
|
+
usleep(1000);
|
137
|
+
break;
|
138
|
+
default:
|
139
|
+
RAISE_SYSERROR("acquiring %slock", lock_name);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
gettimeofday(&endt, NULL);
|
143
|
+
long elapsed = ((endt.tv_sec - startt.tv_sec) * 1000) + ((endt.tv_usec - startt.tv_usec) / 1000);
|
144
|
+
|
145
|
+
if(elapsed > 0) printf(":( acquired %slock for segment %p after %ldms\n", lock_name, seg, elapsed);
|
146
|
+
return NO_ERROR;
|
147
|
+
}
|
148
|
+
*/
|
149
|
+
|
150
|
+
wp_error* wp_lock_release(pthread_rwlock_t* lock) {
|
151
|
+
if(pthread_rwlock_unlock(lock) != 0) RAISE_SYSERROR("releasing lock");
|
152
|
+
return NO_ERROR;
|
153
|
+
}
|