whistlepig 0.9.1 → 0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
}
|