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 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
+ }