chiark / gitweb /
rm some todos
[inn-innduct.git] / storage / tradindexed / tdx-group.c
1 /*  $Id: tdx-group.c 7598 2007-02-09 02:40:51Z eagle $
2 **
3 **  Group index handling for the tradindexed overview method.
4 **
5 **  Implements the handling of the group.index file for the tradindexed
6 **  overview method.  This file contains an entry for every group and stores
7 **  the high and low article marks and the base article numbers for each
8 **  individual group index file.
9 **
10 **  Externally visible functions have a tdx_ prefix; internal functions do
11 **  not.  (Externally visible unfortunately means everything that needs to be
12 **  visible outside of this object file, not just interfaces exported to
13 **  consumers of the overview API.)
14 **
15 **  This code has to support readers and writers sharing the same files, and
16 **  we want to avoid locking where possible since locking may be very slow
17 **  (such as over NFS).  Each group has two data files (and one has to get the
18 **  right index file for a given data file or get mangled results) and one
19 **  piece of data in the main index file required to interpret the individual
20 **  index file, namely the article base of that index.
21 **
22 **  We can make the following assumptions:
23 **
24 **   - The high water mark for a group is monotonically increasing; in other
25 **     words, the highest numbered article in a group won't ever decrease.
26 **
27 **   - While the article base may either increase or decrease, it will never
28 **     change unless the inode of the index file on disk also changes, since
29 **     changing the base requires rewriting the index file.
30 **
31 **   - No two files will have the same inode (this requirement should be safe
32 **     even in strange Unix file formats, since the files are all in the same
33 **     directory).
34 **
35 **  We therefore use the following procedure to update the data:  The high
36 **  water mark may be changed at any time but surrounded in a write lock.  The
37 **  base may only be changed as part of an index rebuild.  To do an index
38 **  rebuild, we follow the following procedure:
39 **
40 **   1) Obtain a write lock on the group entry in the main index.
41 **   2) Write out new index and data files to new temporary file names.
42 **   3) Store the new index inode into the main index.
43 **   4) Update the high, low, and base article numbers in the main index.
44 **   5) Rename the data file to its correct name.
45 **   6) Rename the index file to its correct name.
46 **   7) Release the write lock.
47 **
48 **  We use the following procedure to read the data:
49 **
50 **   1) Open the group data files (both index and data).
51 **   2) Store copies of the current high water mark and base in variables.
52 **   3) Check to be sure the index inode matches the master index file.
53 **
54 **  If it does match, then we have a consistent set of data, since the high
55 **  water mark and base values have to match the index we have (the inode
56 **  value is updated first).  It may not be the most current set of data, but
57 **  since we have those index and data files open, even if they're later
58 **  rebuilt we'll continue looking at the same files.  They may have further
59 **  data appended to them, but that's safe.
60 **
61 **  If the index inode doesn't match, someone's rebuilt the file while we were
62 **  trying to open it.  Continue with the following procedure:
63 **
64 **   4) Close the data files that we opened.
65 **   5) Obtain a read lock on the group entry in the main index.
66 **   6) Reopen the data files.
67 **   7) Grab the current high water mark and base.
68 **   8) Release the read lock.
69 **
70 **  In other words, if there appears to be contention, we fall back to using
71 **  locking so that we don't try to loop (which also avoids an infinite loop
72 **  in the event of corruption of the main index).
73 **
74 **  Note that once we have a consistent set of data files open, we don't need
75 **  to aggressively check for new data files until someone asks for an article
76 **  outside the range of articles that we know about.  We may be working from
77 **  outdated data files, but the most we'll miss is a cancel or an expiration
78 **  run.  Overview data doesn't change; new data is appended and old data is
79 **  expired.  We can afford to check only every once in a while, just to be
80 **  sure that we're not going to hand out overview data for a bunch of expired
81 **  articles.
82 */
83
84 #include "config.h"
85 #include "clibrary.h"
86 #include "portable/mmap.h"
87 #include <errno.h>
88 #include <fcntl.h>
89 #include <limits.h>
90 #include <sys/stat.h>
91 #include <time.h>
92
93 #include "inn/hashtab.h"
94 #include "inn/innconf.h"
95 #include "inn/messages.h"
96 #include "inn/mmap.h"
97 #include "inn/qio.h"
98 #include "inn/vector.h"
99 #include "libinn.h"
100 #include "paths.h"
101 #include "tdx-private.h"
102 #include "tdx-structure.h"
103
104 /* Returned to callers as an opaque data type, this stashes all of the
105    information about an open group.index file. */
106 struct group_index {
107     char *path;
108     int fd;
109     bool writable;
110     struct group_header *header;
111     struct group_entry *entries;
112     int count;
113 };
114
115 /* Forward declaration. */
116 struct hashmap;
117
118 /* Internal prototypes. */
119 static int index_entry_count(size_t size);
120 static size_t index_file_size(int count);
121 static bool index_lock(int fd, enum inn_locktype type);
122 static bool index_lock_group(int fd, ptrdiff_t offset, enum inn_locktype);
123 static bool index_map(struct group_index *);
124 static bool index_maybe_remap(struct group_index *, long loc);
125 static void index_unmap(struct group_index *);
126 static bool index_expand(struct group_index *);
127 static long index_find(struct group_index *, const char *group);
128
129
130 /*
131 **  Given a file size, return the number of group entries that it contains.
132 */
133 static int
134 index_entry_count(size_t size)
135 {
136     return (size - sizeof(struct group_header)) / sizeof(struct group_entry);
137 }
138
139
140 /*
141 **  Given a number of group entries, return the required file size.
142 */
143 static size_t
144 index_file_size(int count)
145 {
146     return sizeof(struct group_header) + count * sizeof(struct group_entry);
147 }
148
149
150 /*
151 **  Lock the hash table for the group index, used to acquire global locks on
152 **  the group index when updating it.
153 */
154 static bool
155 index_lock(int fd, enum inn_locktype type)
156 {
157     bool status;
158
159     status = inn_lock_range(fd, type, true, 0, sizeof(struct group_header));
160     if (!status)
161         syswarn("tradindexed: cannot %s index hash table",
162                 (type == INN_LOCK_UNLOCK) ? "unlock" : "lock");
163     return status;
164 }
165
166
167 /*
168 **  Lock the group entry for a particular group.  Takes the offset of that
169 **  group entry from the start of the group entries (not the start of the
170 **  file; we have to add the size of the group header).  Used for coordinating
171 **  updates of the data for a group.
172 */
173 static bool
174 index_lock_group(int fd, ptrdiff_t offset, enum inn_locktype type)
175 {
176     bool status;
177     size_t size;
178
179     size = sizeof(struct group_entry);
180     offset = offset * size + sizeof(struct group_header);
181     status = inn_lock_range(fd, type, true, offset, size);
182     if (!status)
183         syswarn("tradindexed: cannot %s group entry at %lu",
184                 (type == INN_LOCK_UNLOCK) ? "unlock" : "lock",
185                 (unsigned long) offset);
186     return status;
187 }
188
189
190 /*
191 **  Memory map (or read into memory) the key portions of the group.index
192 **  file.  Takes a struct group_index to fill in and returns true on success
193 **  and false on failure.
194 */
195 static bool
196 index_map(struct group_index *index)
197 {
198     if (!innconf->tradindexedmmap && index->writable) {
199         warn("tradindexed: cannot open for writing without mmap");
200         return false;
201     }
202
203     if (!innconf->tradindexedmmap) {
204         ssize_t header_size;
205         ssize_t entry_size;
206
207         header_size = sizeof(struct group_header);
208         entry_size = index->count * sizeof(struct group_entry);
209         index->header = xmalloc(header_size);
210         index->entries = xmalloc(entry_size);
211         if (read(index->fd, index->header, header_size) != header_size) {
212             syswarn("tradindexed: cannot read header from %s", index->path);
213             goto fail;
214         }
215         if (read(index->fd, index->entries, entry_size) != entry_size) {
216             syswarn("tradindexed: cannot read entries from %s", index->path);
217             goto fail;
218         }
219         return true;
220
221     fail:
222         free(index->header);
223         free(index->entries);
224         index->header = NULL;
225         index->entries = NULL;
226         return false;
227
228     } else {
229         char *data;
230         size_t size;
231         int flag = PROT_READ;
232
233         if (index->writable)
234             flag = PROT_READ | PROT_WRITE;
235         size = index_file_size(index->count);
236         data = mmap(NULL, size, flag, MAP_SHARED, index->fd, 0);
237         if (data == MAP_FAILED) {
238             syswarn("tradindexed: cannot mmap %s", index->path);
239             return false;
240         }
241         index->header = (struct group_header *)(void *) data;
242         index->entries = (struct group_entry *)
243             (void *)(data + sizeof(struct group_header));
244         return true;
245     }
246 }
247
248
249 static bool
250 file_open_group_index(struct group_index *index, struct stat *st)
251 {
252     int open_mode;
253
254     index->header = NULL;
255     open_mode = index->writable ? O_RDWR | O_CREAT : O_RDONLY;
256     index->fd = open(index->path, open_mode, ARTFILE_MODE);
257     if (index->fd < 0) {
258         syswarn("tradindexed: cannot open %s", index->path);
259         goto fail;
260     }
261
262     if (fstat(index->fd, st) < 0) {
263         syswarn("tradindexed: cannot fstat %s", index->path);
264         goto fail;
265     }
266     close_on_exec(index->fd, true);
267     return true;
268
269  fail:
270     if (index->fd >= 0) {
271         close(index->fd);
272         index->fd = -1;
273     }
274     return false;
275 }
276
277
278 /*
279 **  Given a group location, remap the index file if our existing mapping isn't
280 **  large enough to include that group.  (This can be the case when another
281 **  writer is appending entries to the group index.)
282 */
283 static bool
284 index_maybe_remap(struct group_index *index, long loc)
285 {
286     struct stat st;
287     int count;
288     int r;
289
290     if (loc < index->count)
291         return true;
292
293     /* Don't remap if remapping wouldn't actually help. */
294     r = fstat(index->fd, &st);
295     if (r == -1) {
296         if (errno == ESTALE) {
297             index_unmap(index);
298             if (!file_open_group_index(index, &st))
299                 return false;
300         } else {
301             syswarn("tradindexed: cannot stat %s", index->path);
302             return false;
303         }
304     }
305     count = index_entry_count(st.st_size);
306     if (count < loc && index->header != NULL)
307         return true;
308
309     /* Okay, remapping will actually help. */
310     index_unmap(index);
311     index->count = count;
312     return index_map(index);
313 }
314
315
316 /*
317 **  Unmap the index file, either in preparation for closing the overview
318 **  method or to get ready to remap it.  We warn about failures to munmap but
319 **  don't do anything about them; there isn't much that we can do.
320 */
321 static void
322 index_unmap(struct group_index *index)
323 {
324     if (index->header == NULL)
325         return;
326     if (!innconf->tradindexedmmap) {
327         free(index->header);
328         free(index->entries);
329     } else {
330         if (munmap(index->header, index_file_size(index->count)) < 0)
331             syswarn("tradindexed: cannot munmap %s", index->path);
332     }
333     index->header = NULL;
334     index->entries = NULL;
335 }
336
337
338 /*
339 **  Expand the group.index file to hold more entries; also used to build the
340 **  initial file.  The caller is expected to lock the group index.
341 */
342 static bool
343 index_expand(struct group_index *index)
344 {
345     int i;
346
347     index_unmap(index);
348     index->count += 1024;
349     if (ftruncate(index->fd, index_file_size(index->count)) < 0) {
350         syswarn("tradindexed: cannot expand %s", index->path);
351         return false;
352     }
353
354     /* If mapping the index fails, we've already extended it but we haven't
355        done anything with the new portion of the file.  That means that it's
356        all zeroes, which means that it contains index entries who all think
357        their next entry is entry 0.  We don't want to leave things in this
358        state (particularly if this was the first expansion of the index file,
359        in which case entry 0 points to entry 0 and our walking functions may
360        go into infinite loops.  Undo the file expansion. */
361     if (!index_map(index)) {
362         index->count -= 1024;
363         if (ftruncate(index->fd, index_file_size(index->count)) < 0) {
364             syswarn("tradindexed: cannot shrink %s", index->path);
365         }
366         return false;
367     }
368
369     /* If the magic isn't right, assume this is a new index file. */
370     if (index->header->magic != TDX_MAGIC) {
371         index->header->magic = TDX_MAGIC;
372         index->header->freelist.recno = -1;
373         for (i = 0; i < TDX_HASH_SIZE; i++)
374             index->header->hash[i].recno = -1;
375     }
376
377     /* Walk the new entries back to front, adding them to the free list. */
378     for (i = index->count - 1; i >= index->count - 1024; i--) {
379         index->entries[i].next = index->header->freelist;
380         index->header->freelist.recno = i;
381     }
382
383     inn_mapcntl(index->header, index_file_size(index->count), MS_ASYNC);
384     return true;
385 }
386
387
388 /*
389 **  Open the group.index file and allocate a new struct for it, returning a
390 **  pointer to that struct.  Takes a bool saying whether or not the overview
391 **  should be opened for write.
392 */
393 struct group_index *
394 tdx_index_open(bool writable)
395 {
396     struct group_index *index;
397     struct stat st;
398
399     index = xmalloc(sizeof(struct group_index));
400     index->path = concatpath(innconf->pathoverview, "group.index");
401     index->writable = writable;
402     if (!file_open_group_index(index, &st)) {
403         goto fail;
404     }
405     if ((size_t) st.st_size > sizeof(struct group_header)) {
406         index->count = index_entry_count(st.st_size);
407         if (!index_map(index))
408             goto fail;
409     } else {
410         index->count = 0;
411         if (index->writable) {
412             if (st.st_size > 0)
413                 warn("tradindexed: recreating truncated %s", index->path);
414             if (!index_expand(index))
415                 goto fail;
416         } else {
417             index->header = NULL;
418             index->entries = NULL;
419         }
420     }
421     return index;
422
423  fail:
424     tdx_index_close(index);
425     return NULL;
426 }
427
428
429 /*
430 **  Given a group name hash, return an index into the hash table in the
431 **  group.index header.
432 */
433 static long
434 index_bucket(HASH hash)
435 {
436     unsigned int bucket;
437
438     memcpy(&bucket, &hash, sizeof(bucket));
439     return bucket % TDX_HASH_SIZE;
440 }
441
442
443 /*
444 **  Given a pointer to a group entry, return its location number.
445 */
446 static long
447 entry_loc(const struct group_index *index, const struct group_entry *entry)
448 {
449     return entry - index->entries;
450 }
451
452
453 /*
454 **  Splice out a particular group entry.  Takes the entry and a pointer to the
455 **  location where a pointer to it is stored.
456 */
457 static void
458 entry_splice(struct group_entry *entry, int *parent)
459 {
460     *parent = entry->next.recno;
461     entry->next.recno = -1;
462     inn_mapcntl(parent, sizeof(*parent), MS_ASYNC);
463 }
464
465
466 /*
467 **  Add a new entry to the appropriate hash chain.
468 */
469 static void
470 index_add(struct group_index *index, struct group_entry *entry)
471 {
472     long bucket, loc;
473
474     bucket = index_bucket(entry->hash);
475     loc = entry_loc(index, entry);
476     if (loc == index->header->hash[bucket].recno) {
477         warn("tradindexed: refusing to add a loop for %ld in bucket %ld",
478              loc, bucket);
479         return;
480     }
481     entry->next.recno = index->header->hash[bucket].recno;
482     index->header->hash[bucket].recno = entry_loc(index, entry);
483     inn_mapcntl(&index->header->hash[bucket], sizeof(struct loc), MS_ASYNC);
484     inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
485 }
486
487
488 /*
489 **  Find a group in the index file, returning the group number for that group
490 **  or -1 if the group can't be found.
491 */
492 static long
493 index_find(struct group_index *index, const char *group)
494 {
495     HASH hash;
496     long loc;
497
498     if (index->header == NULL || index->entries == NULL)
499         return -1;
500     hash = Hash(group, strlen(group));
501     if (innconf->nfsreader && !index_maybe_remap(index, LONG_MAX))
502         return -1;
503     loc = index->header->hash[index_bucket(hash)].recno;
504
505     while (loc >= 0 && loc < index->count) {
506         struct group_entry *entry;
507
508         if (loc > index->count && !index_maybe_remap(index, loc))
509             return -1;
510         entry = index->entries + loc;
511         if (entry->deleted == 0)
512             if (memcmp(&hash, &entry->hash, sizeof(hash)) == 0)
513                 return loc;
514         if (loc == entry->next.recno) {
515             syswarn("tradindexed: index loop for entry %ld", loc);
516             return -1;
517         }
518         loc = entry->next.recno;
519     }
520     return -1;
521 }
522
523
524 /*
525 **  Add a given entry to the free list.
526 */
527 static void
528 freelist_add(struct group_index *index, struct group_entry *entry)
529 {
530     entry->next.recno = index->header->freelist.recno;
531     index->header->freelist.recno = entry_loc(index, entry);
532     inn_mapcntl(&index->header->freelist, sizeof(struct loc), MS_ASYNC);
533     inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
534 }
535
536
537 /*
538 **  Find an entry by hash value (rather than group name) and splice it out of
539 **  whatever chain it might belong to.  This function is called by both
540 **  index_unlink and index_audit_group.  Locking must be done by the caller.
541 **  Returns the group location of the spliced group.
542 */
543 static long
544 index_unlink_hash(struct group_index *index, HASH hash)
545 {
546     int *parent;
547     long current;
548
549     parent = &index->header->hash[index_bucket(hash)].recno;
550     current = *parent;
551
552     while (current >= 0 && current < index->count) {
553         struct group_entry *entry;
554
555         if (current > index->count && !index_maybe_remap(index, current))
556             return -1;
557         entry = &index->entries[current];
558         if (entry->deleted == 0)
559             if (memcmp(&hash, &entry->hash, sizeof(hash)) == 0) {
560                 entry_splice(entry, parent);
561                 return current;
562             }
563         if (current == entry->next.recno) {
564             syswarn("tradindexed: index loop for entry %ld", current);
565             return -1;
566         }
567         parent = &entry->next.recno;
568         current = *parent;
569     }
570     return -1;
571 }
572
573
574 /*
575 **  Like index_find, but also removes that entry out of whatever chain it
576 **  might belong to.  This function is called by tdx_index_delete.  Locking
577 **  must be done by the caller.
578 */
579 static long
580 index_unlink(struct group_index *index, const char *group)
581 {
582     HASH hash;
583
584     hash = Hash(group, strlen(group));
585     return index_unlink_hash(index, hash);
586 }
587
588
589 /*
590 **  Return the information stored about a given group in the group index.
591 */
592 struct group_entry *
593 tdx_index_entry(struct group_index *index, const char *group)
594 {
595     long loc;
596     struct group_entry *entry;
597
598     loc = index_find(index, group);
599     if (loc == -1)
600         return NULL;
601     entry = index->entries + loc;
602     if (innconf->tradindexedmmap && innconf->nfsreader)
603         inn_mapcntl(entry, sizeof *entry, MS_INVALIDATE);
604     return entry;
605 }
606
607
608 /*
609 **  Add a new newsgroup to the group.index file.  Takes the newsgroup name,
610 **  its high and low water marks, and the newsgroup flag.  Note that aliased
611 **  newsgroups are not currently handled.  If the group already exists, just
612 **  update the flag (not the high and low water marks).
613 */
614 bool
615 tdx_index_add(struct group_index *index, const char *group, ARTNUM low,
616               ARTNUM high, const char *flag)
617 {
618     HASH hash;
619     long loc;
620     struct group_entry *entry;
621     struct group_data *data;
622
623     if (!index->writable)
624         return false;
625
626     /* If the group already exists, update the flag as necessary and then
627        we're all done. */
628     loc = index_find(index, group);
629     if (loc != -1) {
630         entry = &index->entries[loc];
631         if (entry->flag != *flag) {
632             entry->flag = *flag;
633             inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
634         }
635         return true;
636     }
637
638     index_lock(index->fd, INN_LOCK_WRITE);
639
640     /* Find a free entry.  If we don't have any free space, make some. */
641     if (index->header->freelist.recno == -1)
642         if (!index_expand(index)) {
643             index_lock(index->fd, INN_LOCK_UNLOCK);
644             return false;
645         }
646     loc = index->header->freelist.recno;
647     index->header->freelist.recno = index->entries[loc].next.recno;
648     inn_mapcntl(&index->header->freelist, sizeof(struct loc), MS_ASYNC);
649
650     /* Initialize the entry. */
651     entry = &index->entries[loc];
652     hash = Hash(group, strlen(group));
653     entry->hash = hash;
654     entry->low = (low == 0 && high != 0) ? high + 1 : low;
655     entry->high = high;
656     entry->deleted = 0;
657     entry->base = 0;
658     entry->count = 0;
659     entry->flag = *flag;
660     data = tdx_data_new(group, index->writable);
661     if (!tdx_data_open_files(data))
662         warn("tradindexed: unable to create data files for %s", group);
663     entry->indexinode = data->indexinode;
664     tdx_data_close(data);
665     index_add(index, entry);
666
667     index_lock(index->fd, INN_LOCK_UNLOCK);
668     return true;
669 }
670
671
672 /*
673 **  Delete a group index entry.
674 */
675 bool
676 tdx_index_delete(struct group_index *index, const char *group)
677 {
678     long loc;
679     struct group_entry *entry;
680
681     if (!index->writable)
682         return false;
683
684     /* Lock the header for the entire operation, mostly as prevention against
685        interfering with ongoing audits (which lock while they're running). */
686     index_lock(index->fd, INN_LOCK_WRITE);
687
688     /* Splice out the entry and mark it as deleted. */
689     loc = index_unlink(index, group);
690     if (loc == -1) {
691         index_lock(index->fd, INN_LOCK_UNLOCK);
692         return false;
693     }
694     entry = &index->entries[loc];
695     entry->deleted = time(NULL);
696     HashClear(&entry->hash);
697
698     /* Add the entry to the free list. */
699     freelist_add(index, entry);
700     index_lock(index->fd, INN_LOCK_UNLOCK);
701
702     /* Delete the group data files for this group. */
703     tdx_data_delete(group, NULL);
704
705     return true;
706 }
707
708
709 /*
710 **  Close an open handle to the group index file, freeing the group_index
711 **  structure at the same time.  The argument to this function becomes invalid
712 **  after this call.
713 */
714 void
715 tdx_index_close(struct group_index *index)
716 {
717     index_unmap(index);
718     if (index->fd >= 0) {
719         close(index->fd);
720         index->fd = -1;
721     }
722     free(index->path);
723     free(index);
724 }
725
726
727 /*
728 **  Open the data files for a particular group.  The interface to this has to
729 **  be in this file because we have to lock the group and retry if the inode
730 **  of the opened index file doesn't match the one recorded in the group index
731 **  file.  Optionally take a pointer to the group index entry if the caller
732 **  has already gone to the work of finding it.
733 */
734 struct group_data *
735 tdx_data_open(struct group_index *index, const char *group,
736               struct group_entry *entry)
737 {
738     struct group_data *data;
739     ARTNUM high, base;
740     ptrdiff_t offset;
741
742     if (entry == NULL) {
743         entry = tdx_index_entry(index, group);
744         if (entry == NULL)
745             return NULL;
746     }
747     offset = entry - index->entries;
748     data = tdx_data_new(group, index->writable);
749
750     /* Check to see if the inode of the index file matches.  If it doesn't,
751        this probably means that as we were opening the index file, someone
752        else rewrote it (either expire or repack).  Obtain a lock and try
753        again.  If there's still a mismatch, go with what we get; there's some
754        sort of corruption.
755
756        This code is very sensitive to order and parallelism.  See the comment
757        at the beginning of this file for methodology. */
758     if (!tdx_data_open_files(data))
759         goto fail;
760     high = entry->high;
761     base = entry->base;
762     if (entry->indexinode != data->indexinode) {
763         index_lock_group(index->fd, offset, INN_LOCK_READ);
764         if (!tdx_data_open_files(data)) {
765             index_lock_group(index->fd, offset, INN_LOCK_UNLOCK);
766             goto fail;
767         }
768         if (entry->indexinode != data->indexinode)
769             warn("tradindexed: index inode mismatch for %s", group);
770         high = entry->high;
771         base = entry->base;
772         index_lock_group(index->fd, offset, INN_LOCK_UNLOCK);
773     }
774     data->high = high;
775     data->base = base;
776     return data;
777
778  fail:
779     tdx_data_close(data);
780     return NULL;
781 }
782
783
784 /*
785 **  Add an overview record for a particular article.  Takes the group entry,
786 **  the open overview data structure, and the information about the article
787 **  and returns true on success, false on failure.  This function calls
788 **  tdx_data_store to do most of the real work and then updates the index
789 **  information.
790 */
791 bool
792 tdx_data_add(struct group_index *index, struct group_entry *entry,
793              struct group_data *data, const struct article *article)
794 {
795     ARTNUM old_base;
796     ino_t old_inode;
797     ptrdiff_t offset = entry - index->entries;
798
799     if (!index->writable)
800         return false;
801     index_lock_group(index->fd, offset, INN_LOCK_WRITE);
802
803     /* Make sure we have the most current data files and that we have the
804        right base article number. */
805     if (entry->indexinode != data->indexinode) {
806         if (!tdx_data_open_files(data))
807             goto fail;
808         if (entry->indexinode != data->indexinode)
809             warn("tradindexed: index inode mismatch for %s",
810                  HashToText(entry->hash));
811         data->base = entry->base;
812     }
813
814     /* If the article number is too low to store in the group index, repack
815        the group with a lower base index. */
816     if (entry->base > article->number) {
817         if (!tdx_data_pack_start(data, article->number))
818             goto fail;
819         old_inode = entry->indexinode;
820         old_base = entry->base;
821         entry->indexinode = data->indexinode;
822         entry->base = data->base;
823         inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
824         if (!tdx_data_pack_finish(data)) {
825             entry->base = old_base;
826             entry->indexinode = old_inode;
827             inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
828             goto fail;
829         }
830     }
831
832     /* Store the data. */
833     if (!tdx_data_store(data, article))
834         goto fail;
835     if (entry->base == 0)
836         entry->base = data->base;
837     if (entry->low == 0 || entry->low > article->number)
838         entry->low = article->number;
839     if (entry->high < article->number)
840         entry->high = article->number;
841     entry->count++;
842     inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
843     index_lock_group(index->fd, offset, INN_LOCK_UNLOCK);
844     return true;
845
846  fail:
847     index_lock_group(index->fd, offset, INN_LOCK_UNLOCK);
848     return false;
849 }
850
851
852 /*
853 **  Start a rebuild of the group data for a newsgroup.  Right now, all this
854 **  does is lock the group index entry.
855 */
856 bool
857 tdx_index_rebuild_start(struct group_index *index, struct group_entry *entry)
858 {
859     ptrdiff_t offset;
860
861     offset = entry - index->entries;
862     return index_lock_group(index->fd, offset, INN_LOCK_WRITE);
863 }
864
865
866 /*
867 **  Finish a rebuild of the group data for a newsgroup.  Takes the old and new
868 **  entry and writes the data from the new entry into the group index, and
869 **  then unlocks it.
870 */
871 bool
872 tdx_index_rebuild_finish(struct group_index *index, struct group_entry *entry,
873                          struct group_entry *new)
874 {
875     ptrdiff_t offset;
876     ino_t new_inode;
877
878     new_inode = new->indexinode;
879     new->indexinode = entry->indexinode;
880     *entry = *new;
881     entry->indexinode = new_inode;
882     new->indexinode = new_inode;
883     inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
884     offset = entry - index->entries;
885     index_lock_group(index->fd, offset, INN_LOCK_UNLOCK);
886     return true;
887 }
888
889
890 /*
891 **  Expire a single newsgroup.  Most of the work is done by tdx_data_expire*,
892 **  but this routine has the responsibility to do locking (the same as would
893 **  be done for repacking, since the group base may change) and updating the
894 **  group entry.
895 */
896 bool
897 tdx_expire(const char *group, ARTNUM *low, struct history *history)
898 {
899     struct group_index *index;
900     struct group_entry *entry;
901     struct group_entry new_entry;
902     struct group_data *data = NULL;
903     ptrdiff_t offset;
904     ARTNUM old_base;
905     ino_t old_inode;
906
907     index = tdx_index_open(true);
908     if (index == NULL)
909         return false;
910     entry = tdx_index_entry(index, group);
911     if (entry == NULL) {
912         tdx_index_close(index);
913         return false;
914     }
915     tdx_index_rebuild_start(index, entry);
916
917     /* tdx_data_expire_start builds the new IDX and DAT files and fills in the
918        struct group_entry that was passed to it.  tdx_data_rebuild_finish does
919        the renaming of the new files to the final file names. */
920     new_entry = *entry;
921     new_entry.low = 0;
922     new_entry.count = 0;
923     new_entry.base = 0;
924     data = tdx_data_open(index, group, entry);
925     if (data == NULL)
926         goto fail;
927     if (!tdx_data_expire_start(group, data, &new_entry, history))
928         goto fail;
929     old_inode = entry->indexinode;
930     old_base = entry->base;
931     entry->indexinode = new_entry.indexinode;
932     entry->base = new_entry.base;
933     inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
934     tdx_data_close(data);
935     if (!tdx_data_rebuild_finish(group)) {
936         entry->base = old_base;
937         entry->indexinode = old_inode;
938         inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
939         goto fail;
940     }
941
942     /* Almost done.  Update the group index.  If there are no articles in the
943        group, the low water mark should be one more than the high water
944        mark. */
945     if (new_entry.low == 0)
946         new_entry.low = new_entry.high + 1;
947     tdx_index_rebuild_finish(index, entry, &new_entry);
948     if (low != NULL)
949         *low = entry->low;
950     tdx_index_close(index);
951     return true;
952
953  fail:
954     offset = entry - index->entries;
955     index_lock_group(index->fd, offset, INN_LOCK_UNLOCK);
956     if (data != NULL)
957         tdx_data_close(data);
958     tdx_index_close(index);
959     return false;
960 }
961
962
963 /*
964 **  RECOVERY AND AUDITING
965 **
966 **  All code below this point is not used in the normal operations of the
967 **  overview method.  Instead, it's code to dump various data structures or
968 **  audit them for consistency, used by recovery tools and inspection tools.
969 */
970
971 /* Holds a newsgroup name and its hash, used to form a hash table mapping
972    newsgroup hash values to the actual names. */
973 struct hashmap {
974     HASH hash;
975     char *name;
976     char flag;
977 };
978
979 /* Holds information needed by hash traversal functions.  Right now, this is
980    just the pointer to the group index and a flag saying whether to fix
981    problems or not. */
982 struct audit_data {
983     struct group_index *index;
984     bool fix;
985 };
986
987
988 /*
989 **  Hash table functions for the mapping from group hashes to names.
990 */
991 static unsigned long
992 hashmap_hash(const void *entry)
993 {
994     unsigned long hash;
995     const struct hashmap *group = entry;
996
997     memcpy(&hash, &group->hash, sizeof(hash));
998     return hash;
999 }
1000
1001
1002 static const void *
1003 hashmap_key(const void *entry)
1004 {
1005     return &((const struct hashmap *) entry)->hash;
1006 }
1007
1008
1009 static bool
1010 hashmap_equal(const void *key, const void *entry)
1011 {
1012     const HASH *first = key;
1013     const HASH *second;
1014
1015     second = &((const struct hashmap *) entry)->hash;
1016     return memcmp(first, second, sizeof(HASH)) == 0;
1017 }
1018
1019
1020 static void
1021 hashmap_delete(void *entry)
1022 {
1023     struct hashmap *group = entry;
1024
1025     free(group->name);
1026     free(group);
1027 }
1028
1029
1030 /*
1031 **  Construct a hash table of group hashes to group names by scanning the
1032 **  active file.  Returns the constructed hash table.
1033 */
1034 static struct hash *
1035 hashmap_load(void)
1036 {
1037     struct hash *hash;
1038     QIOSTATE *active;
1039     char *activepath, *line;
1040     struct cvector *data = NULL;
1041     struct stat st;
1042     size_t hash_size;
1043     struct hashmap *group;
1044     HASH grouphash;
1045
1046     activepath = concatpath(innconf->pathdb, _PATH_ACTIVE);
1047     active = QIOopen(activepath);
1048     free(activepath);
1049     if (active == NULL)
1050         return NULL;
1051     if (fstat(QIOfileno(active), &st) < 0)
1052         hash_size = 32 * 1024;
1053     else
1054         hash_size = st.st_size / 30;
1055     hash = hash_create(hash_size, hashmap_hash, hashmap_key, hashmap_equal,
1056                        hashmap_delete);
1057
1058     line = QIOread(active);
1059     while (line != NULL) {
1060         data = cvector_split_space(line, data);
1061         if (data->count != 4) {
1062             warn("tradindexed: malformed active file line %s", line);
1063             continue;
1064         }
1065         group = xmalloc(sizeof(struct hashmap));
1066         group->name = xstrdup(data->strings[0]);
1067         group->flag = data->strings[3][0];
1068         grouphash = Hash(group->name, strlen(group->name));
1069         memcpy(&group->hash, &grouphash, sizeof(HASH));
1070         hash_insert(hash, &group->hash, group);
1071         line = QIOread(active);
1072     }
1073     cvector_free(data);
1074     QIOclose(active);
1075     return hash;
1076 }
1077
1078
1079 /*
1080 **  Print the stored information about a single group in human-readable form
1081 **  to stdout.  The format is:
1082 **
1083 **    name high low base count flag deleted inode
1084 **
1085 **  all on one line.  Name is passed into this function.
1086 */
1087 void
1088 tdx_index_print(const char *name, const struct group_entry *entry,
1089                 FILE *output)
1090 {
1091     fprintf(output, "%s %lu %lu %lu %lu %c %lu %lu\n", name, entry->high,
1092             entry->low, entry->base, (unsigned long) entry->count,
1093             entry->flag, (unsigned long) entry->deleted,
1094             (unsigned long) entry->indexinode);
1095 }
1096
1097
1098 /*
1099 **  Dump the complete contents of the group.index file in human-readable form
1100 **  to the specified file, one line per group.
1101 */
1102 void
1103 tdx_index_dump(struct group_index *index, FILE *output)
1104 {
1105     int bucket;
1106     long current;
1107     struct group_entry *entry;
1108     struct hash *hashmap;
1109     struct hashmap *group;
1110     char *name;
1111
1112     if (index->header == NULL || index->entries == NULL)
1113         return;
1114     hashmap = hashmap_load();
1115     for (bucket = 0; bucket < TDX_HASH_SIZE; bucket++) {
1116         current = index->header->hash[bucket].recno;
1117         while (current != -1) {
1118             if (!index_maybe_remap(index, current))
1119                 return;
1120             entry = index->entries + current;
1121             name = NULL;
1122             if (hashmap != NULL) {
1123                 group = hash_lookup(hashmap, &entry->hash);
1124                 if (group != NULL)
1125                     name = group->name;
1126             }
1127             if (name == NULL)
1128                 name = HashToText(entry->hash);
1129             tdx_index_print(name, entry, output);
1130             if (current == entry->next.recno) {
1131                 warn("tradindexed: index loop for entry %ld", current);
1132                 return;
1133             }
1134             current = entry->next.recno;
1135         }
1136     }
1137     if (hashmap != NULL)
1138         hash_free(hashmap);
1139 }
1140
1141
1142 /*
1143 **  Audit a particular group entry location to ensure that it points to a
1144 **  valid entry within the group index file.  Takes a pointer to the location,
1145 **  the number of the location, a pointer to the group entry if any (if not,
1146 **  the location is assumed to be part of the header hash table), and a flag
1147 **  saying whether to fix problems that are found.
1148 */
1149 static void
1150 index_audit_loc(struct group_index *index, int *loc, long number,
1151                 struct group_entry *entry, bool fix)
1152 {
1153     bool error = false;
1154
1155     if (*loc >= index->count) {
1156         warn("tradindexed: out of range index %d in %s %ld",
1157              *loc, (entry == NULL ? "bucket" : "entry"), number);
1158         error = true;
1159     }
1160     if (*loc < 0 && *loc != -1) {
1161         warn("tradindexed: invalid negative index %d in %s %ld",
1162              *loc, (entry == NULL ? "bucket" : "entry"), number);
1163         error = true;
1164     }
1165     if (entry != NULL && *loc == number) {
1166         warn("tradindexed: index loop for entry %ld", number);
1167         error = true;
1168     }
1169
1170     if (fix && error) {
1171         *loc = -1;
1172         inn_mapcntl(loc, sizeof(*loc), MS_ASYNC);
1173     }
1174 }
1175
1176
1177 /*
1178 **  Check an entry to see if it was actually deleted.  Make sure that all the
1179 **  information is consistent with a deleted group if it's not and the fix
1180 **  flag is set.
1181 */
1182 static void
1183 index_audit_deleted(struct group_entry *entry, long number, bool fix)
1184 {
1185     if (entry->deleted != 0 && !HashEmpty(entry->hash)) {
1186         warn("tradindexed: entry %ld has a delete time but a non-zero hash",
1187              number);
1188         if (fix) {
1189             HashClear(&entry->hash);
1190             inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
1191         }
1192     }
1193 }
1194
1195
1196 /*
1197 **  Audit the group header for any inconsistencies.  This checks the
1198 **  reachability of all of the group entries, makes sure that deleted entries
1199 **  are on the free list, and otherwise checks the linked structure of the
1200 **  whole file.  The data in individual entries is not examined.  If the
1201 **  second argument is true, also attempt to fix inconsistencies.
1202 */
1203 static void
1204 index_audit_header(struct group_index *index, bool fix)
1205 {
1206     long bucket, current;
1207     struct group_entry *entry;
1208     int *parent, *next;
1209     bool *reachable;
1210
1211     /* First, walk all of the regular hash buckets, making sure that all of
1212        the group location pointers are valid and sane, that all groups that
1213        have been deleted are correctly marked as such, and that all groups are
1214        in their correct hash chain.  Build reachability information as we go,
1215        used later to ensure that all group entries are reachable. */
1216     reachable = xcalloc(index->count, sizeof(bool));
1217     for (bucket = 0; bucket < TDX_HASH_SIZE; bucket++) {
1218         parent = &index->header->hash[bucket].recno;
1219         index_audit_loc(index, parent, bucket, NULL, fix);
1220         current = *parent;
1221         while (current >= 0 && current < index->count) {
1222             entry = &index->entries[current];
1223             next = &entry->next.recno;
1224             if (entry->deleted == 0 && bucket != index_bucket(entry->hash)) {
1225                 warn("tradindexed: entry %ld is in bucket %ld instead of its"
1226                      " correct bucket %ld", current, bucket,
1227                      index_bucket(entry->hash));
1228                 if (fix) {
1229                     entry_splice(entry, parent);
1230                     next = parent;
1231                 }
1232             } else {
1233                 if (reachable[current])
1234                     warn("tradindexed: entry %ld is reachable from multiple"
1235                          " paths", current);
1236                 reachable[current] = true;
1237             }
1238             index_audit_deleted(entry, current, fix);
1239             index_audit_loc(index, &entry->next.recno, current, entry, fix);
1240             if (entry->deleted != 0) {
1241                 warn("tradindexed: entry %ld is deleted but not in the free"
1242                      " list", current);
1243                 if (fix) {
1244                     entry_splice(entry, parent);
1245                     next = parent;
1246                     reachable[current] = false;
1247                 }
1248             }
1249             if (*next == current)
1250                 break;
1251             parent = next;
1252             current = *parent;
1253         }
1254     }
1255
1256     /* Now, walk the free list.  Make sure that each group in the free list is
1257        actually deleted, and update the reachability information. */
1258     index_audit_loc(index, &index->header->freelist.recno, 0, NULL, fix);
1259     parent = &index->header->freelist.recno;
1260     current = *parent;
1261     while (current >= 0 && current < index->count) {
1262         entry = &index->entries[current];
1263         index_audit_deleted(entry, current, fix);
1264         reachable[current] = true;
1265         if (!HashEmpty(entry->hash) && entry->deleted == 0) {
1266             warn("tradindexed: undeleted entry %ld in free list", current);
1267             if (fix) {
1268                 entry_splice(entry, parent);
1269                 reachable[current] = false;
1270             }
1271         }
1272         index_audit_loc(index, &entry->next.recno, current, entry, fix);
1273         if (entry->next.recno == current)
1274             break;
1275         parent = &entry->next.recno;
1276         current = *parent;
1277     }
1278
1279     /* Finally, check all of the unreachable entries and if fix is true, try
1280        to reattach them in the appropriate location. */
1281     for (current = 0; current < index->count; current++)
1282         if (!reachable[current]) {
1283             warn("tradindexed: unreachable entry %ld", current);
1284             if (fix) {
1285                 entry = &index->entries[current];
1286                 if (!HashEmpty(entry->hash) && entry->deleted == 0)
1287                     index_add(index, entry);
1288                 else {
1289                     HashClear(&entry->hash);
1290                     entry->deleted = 0;
1291                     freelist_add(index, entry);
1292                 }
1293             }
1294         }
1295
1296     /* All done. */
1297     free(reachable);
1298 }
1299
1300
1301 /*
1302 **  Audit a particular group entry for any inconsistencies.  This doesn't
1303 **  check any of the structure, or whether the group is deleted, just the data
1304 **  as stored in the group data files (mostly by calling tdx_data_audit to do
1305 **  the real work).  Note that while the low water mark may be updated, the
1306 **  high water mark is left unchanged.
1307 */
1308 static void
1309 index_audit_group(struct group_index *index, struct group_entry *entry,
1310                   struct hash *hashmap, bool fix)
1311 {
1312     struct hashmap *group;
1313     ptrdiff_t offset;
1314
1315     offset = entry - index->entries;
1316     index_lock_group(index->fd, offset, INN_LOCK_WRITE);
1317     group = hash_lookup(hashmap, &entry->hash);
1318     if (group == NULL) {
1319         warn("tradindexed: group %ld not found in active file",
1320              entry_loc(index, entry));
1321         if (fix) {
1322             index_unlink_hash(index, entry->hash);
1323             HashClear(&entry->hash);
1324             entry->deleted = time(NULL);
1325             freelist_add(index, entry);
1326         }
1327     } else {
1328         if (entry->flag != group->flag) {
1329             entry->flag = group->flag;
1330             inn_mapcntl(entry, sizeof(*entry), MS_ASYNC);
1331         }
1332         tdx_data_audit(group->name, entry, fix);
1333     }
1334     index_lock_group(index->fd, offset, INN_LOCK_UNLOCK);
1335 }
1336
1337
1338 /*
1339 **  Check to be sure that a given group exists in the overview index, and if
1340 **  missing, adds it.  Assumes that the index isn't locked, since it calls the
1341 **  normal functions for adding new groups (this should only be called after
1342 **  the index has already been repaired, for the same reason).  Called as a
1343 **  hash traversal function, walking the hash table of groups from the active
1344 **  file.
1345 */
1346 static void
1347 index_audit_active(void *value, void *cookie)
1348 {
1349     struct hashmap *group = value;
1350     struct audit_data *data = cookie;
1351     struct group_entry *entry;
1352
1353     entry = tdx_index_entry(data->index, group->name);
1354     if (entry == NULL) {
1355         warn("tradindexed: group %s missing from overview", group->name);
1356         if (data->fix)
1357             tdx_index_add(data->index, group->name, 0, 0, &group->flag);
1358     }
1359 }
1360
1361
1362 /*
1363 **  Audit the group index for any inconsistencies.  If the argument is true,
1364 **  also attempt to fix those inconsistencies.
1365 */
1366 void
1367 tdx_index_audit(bool fix)
1368 {
1369     struct group_index *index;
1370     struct stat st;
1371     off_t expected;
1372     int count;
1373     struct hash *hashmap;
1374     long bucket;
1375     struct group_entry *entry;
1376     struct audit_data data;
1377
1378     index = tdx_index_open(true);
1379     if (index == NULL)
1380         return;
1381
1382     /* Keep a lock on the header through the whole audit process.  This will
1383        stall any newgroups or rmgroups, but not normal article reception.  We
1384        don't want the structure of the group entries changing out from under
1385        us, although we don't mind if the data does until we're validating that
1386        particular group. */
1387     index_lock(index->fd, INN_LOCK_WRITE);
1388
1389     /* Make sure the size looks sensible. */
1390     if (fstat(index->fd, &st) < 0) {
1391         syswarn("tradindexed: cannot fstat %s", index->path);
1392         return;
1393     }
1394     count = index_entry_count(st.st_size);
1395     expected = index_file_size(count);
1396     if (expected != st.st_size) {
1397         syswarn("tradindexed: %ld bytes of trailing trash in %s",
1398                 (unsigned long) (st.st_size - expected), index->path);
1399         if (fix)
1400             if (ftruncate(index->fd, expected) < 0)
1401                 syswarn("tradindexed: cannot truncate %s", index->path);
1402     }
1403     index_maybe_remap(index, count);
1404
1405     /* Okay everything is now mapped and happy.  Validate the header. */
1406     index_audit_header(index, fix);
1407     index_lock(index->fd, INN_LOCK_UNLOCK);
1408
1409     /* Walk all the group entries and check them individually.  To do this, we
1410        need to map hashes to group names, so load a hash of the active file to
1411        do that resolution. */
1412     hashmap = hashmap_load();
1413     data.index = index;
1414     data.fix = fix;
1415     hash_traverse(hashmap, index_audit_active, &data);
1416     for (bucket = 0; bucket < index->count; bucket++) {
1417         entry = &index->entries[bucket];
1418         if (HashEmpty(entry->hash) || entry->deleted != 0)
1419             continue;
1420         index_audit_group(index, entry, hashmap, fix);
1421     }
1422     if (hashmap != NULL)
1423         hash_free(hashmap);
1424 }