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 CHANGED
@@ -1,14 +1,14 @@
1
1
  = Whistlepig
2
2
 
3
- Whistlepig is a minimalist realtime full-text search index. Its goal is to be
4
- as small and feature-free as possible, while still remaining useful, performant
5
- and scalable to large corpora. If you want realtime full-text search without
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.9.1, released 2012-03-14.
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 to an extreme.
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
- == A note on concurrency:
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
- Whistlepig is currently single-process and single-thread only. However, it is
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 send bug reports and comments to: wmorgan-whistlepig-readme@masanjin.net.
120
+ Please file bugs here: https://github.com/wmorgan/whistlepig/issues
121
+ Please send comments to: wmorgan-whistlepig-readme@masanjin.net.
@@ -1,6 +1,6 @@
1
1
  require 'mkmf'
2
2
 
3
- $CFLAGS = "-g -O3 -std=c99 $(cflags) -D_ANSI_SOURCE"
3
+ $CFLAGS= "-std=c99 -D_ANSI_SOURCE -D_XOPEN_SOURCE=600 $(cflags)"
4
4
 
5
5
  create_header
6
6
  create_makefile "whistlepig/whistlepig"
@@ -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
- RAISING_STATIC(ensure_num_segments(wp_index* index)) {
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 *= 2;
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
- wp_error* wp_index_load(wp_index** indexptr, const char* pathname_base) {
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
- wp_index* index = *indexptr = malloc(sizeof(wp_index));
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
- index->pathname_base = pathname_base;
55
- index->num_segments = 0;
56
- index->sizeof_segments = 1;
57
- index->open = 1;
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
- // load all the segments we can
62
- while(index->num_segments < WP_MAX_SEGMENTS) {
63
- snprintf(buf, PATH_BUF_SIZE, "%s%d", pathname_base, index->num_segments);
64
- if(!wp_segment_exists(buf)) break;
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
- RELAY_ERROR(ensure_num_segments(index));
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
- postings_region* prevpr = MMAP_OBJ(index->segments[index->num_segments - 1].postings, postings_region);
75
- index->docid_offsets[index->num_segments] = prevpr->num_docs + index->docid_offsets[index->num_segments - 1];
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
- RELAY_ERROR(wp_search_init_search_state(query, &index->segments[query->segment_idx]));
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
- RELAY_ERROR(wp_search_run_query_on_segment(query, &index->segments[query->segment_idx], want_num_results, &got_num_results, segment_results));
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
- wp_error* wp_index_add_entry(wp_index* index, wp_entry* entry, uint64_t* doc_id) {
172
- int success;
173
- wp_segment* seg = &index->segments[index->num_segments - 1];
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
- // first, ensure we have enough space in the current segment
176
- uint32_t postings_bytes;
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 not, we need to open a new one
181
- if(!success) {
182
- DEBUG("segment %d is full, loading a new one", index->num_segments - 1);
183
- char buf[PATH_BUF_SIZE];
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
- RELAY_ERROR(wp_segment_dumpinfo(&index->segments[i], stream));
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(wp_segment_add_label(&index->segments[i - 1], label, (docid_t)(doc_id - index->docid_offsets[i - 1])));
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(wp_segment_remove_label(&index->segments[i - 1], label, (docid_t)(doc_id - index->docid_offsets[i - 1])));
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
- uint64_t wp_index_num_docs(wp_index* index) {
289
- uint64_t ret = 0;
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--) ret += wp_segment_num_docs(&index->segments[i - 1]);
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 ret;
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
@@ -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
- struct wp_segment* segments;
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
- uint64_t wp_index_num_docs(wp_index* index);
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
+ }