chiark / gitweb /
3205ff0612f870e120ea819fa3d4b48c29d7ea63
[chiark-tcl.git] / cdb / writeable.c
1 /**/
2
3 #include "chiark_tcl_cdb.h"
4
5 #define ftello ftell
6 #define fseeko fseek
7
8 /*---------- Forward declarations ----------*/
9
10 struct ht_forall_ctx;
11
12 /*---------- Useful routines ----------*/
13
14 static void maybe_close(int fd) {
15   if (fd>=0) close(fd);
16 }
17
18 #define PE(m) do{                                               \
19     rc= cht_posixerr(ip, errno, "failed to " m); goto x_rc;     \
20   }while(0)
21
22 /*==================== Subsystems and subtypes ====================*/
23
24 /*---------- Pathbuf ----------*/
25
26 typedef struct Pathbuf {
27   char *buf, *sfx;
28 } Pathbuf;
29
30 #define MAX_SUFFIX 4
31
32 static void pathbuf_init(Pathbuf *pb, const char *pathb) {
33   int l= strlen(pathb);
34   pb->buf= TALLOC(l + 4);
35   memcpy(pb->buf, pathb, l);
36   pb->sfx= pb->buf + l;
37   *pb->sfx++= '.';
38 }
39 static const char *pathbuf_sfx(Pathbuf *pb, const char *suffix) {
40   assert(strlen(suffix) <= MAX_SUFFIX);
41   strcpy(pb->sfx, suffix);
42   return pb->buf;
43 }
44 static void pathbuf_free(Pathbuf *pb) {
45   TFREE(pb->buf);
46   pb->buf= 0;
47 }
48
49 /*---------- Our hash table ----------*/
50
51 typedef struct HashTable {
52   Tcl_HashTable t;
53   Byte padding[128]; /* allow for expansion by Tcl, urgh */
54   Byte confound[16];
55 } HashTable;
56
57 typedef struct HashValue {
58   int len;
59   Byte data[1];
60 } HashValue;
61
62 static HashValue *htv_prep(int len) {
63   HashValue *hd;
64   hd= TALLOC((hd->data - (Byte*)hd) + len);
65   hd->len= len;
66   return hd;
67 }  
68 static Byte *htv_fillptr(HashValue *hd) {
69   return hd->data;
70 }
71
72 static void ht_setup(HashTable *ht) {
73   Tcl_InitHashTable(&ht->t, TCL_STRING_KEYS);
74 }
75 static void ht_update(HashTable *ht, const char *key, HashValue *val_eat) {
76   Tcl_HashEntry *he;
77   int new;
78
79   he= Tcl_CreateHashEntry(&ht->t, (char*)key, &new);
80   if (!new) TFREE(Tcl_GetHashValue(he));
81   Tcl_SetHashValue(he, val_eat);
82     /* eats the value since the data structure owns the memory */
83 }
84 static void ht_maybeupdate(HashTable *ht, const char *key,
85                            HashValue *val_eat) {
86   /* like ht_update except does not overwrite existing values */
87   Tcl_HashEntry *he;
88   int new;
89
90   he= Tcl_CreateHashEntry(&ht->t, (char*)key, &new);
91   if (!new) { TFREE(val_eat); return; }
92   Tcl_SetHashValue(he, val_eat);
93 }
94
95 static int ht_forall(HashTable *ht,
96                      int (*fn)(const char *key, HashValue *val,
97                                struct ht_forall_ctx *ctx),
98                      struct ht_forall_ctx *ctx) {
99   /* Returns first positive value returned by any call to fn, or 0. */
100   Tcl_HashSearch sp;
101   Tcl_HashEntry *he;
102   const char *key;
103   HashValue *val;
104   int r;
105   
106   for (he= Tcl_FirstHashEntry(&ht->t, &sp);
107        he;
108        he= Tcl_NextHashEntry(&sp)) {
109     val= Tcl_GetHashValue(he);
110     if (!val->len) continue;
111
112     key= Tcl_GetHashKey(&ht->t, he);
113     
114     r= fn(key, val, ctx);
115     if (r) return r;
116   }
117   return 0;
118 }
119
120 static void ht_destroy(HashTable *ht) {
121   Tcl_HashSearch sp;
122   Tcl_HashEntry *he;
123   
124   for (he= Tcl_FirstHashEntry(&ht->t, &sp);
125        he;
126        he= Tcl_NextHashEntry(&sp)) {
127     /* ht_forall skips empty (deleted) entries so is no good for this */
128     TFREE(Tcl_GetHashValue(he));
129   }
130   Tcl_DeleteHashTable(&ht->t);
131 }
132
133 /*==================== Existential ====================*/
134
135 /*---------- Rw data structure ----------*/
136
137 typedef struct Rw {
138   int ix, autocompact;
139   int cdb_fd, lock_fd;
140   struct cdb cdb; /* valid iff cdb_fd >= 0 */
141   FILE *logfile;
142   HashTable logincore;
143   Pathbuf pbsome, pbother;
144   off_t mainsz;
145   ScriptToInvoke on_info, on_lexminval;
146 } Rw;
147
148 static int rw_close(Tcl_Interp *ip, Rw *rw) {
149   int rc, r;
150
151   rc= TCL_OK;
152   ht_destroy(&rw->logincore);
153   if (rw->cdb_fd >= 0) cdb_free(&rw->cdb);
154   maybe_close(rw->cdb_fd);
155   maybe_close(rw->lock_fd);
156
157   if (rw->logfile) {
158     r= fclose(rw->logfile);
159     if (r && ip) { rc= cht_posixerr(ip, errno, "probable data loss! failed to"
160                                     " fclose logfile during untidy close"); }
161   }
162
163   pathbuf_free(&rw->pbsome); pathbuf_free(&rw->pbother);
164   TFREE(rw);
165   return rc;
166 }
167
168 static void destroy_cdbrw_idtabcb(Tcl_Interp *ip, void *rw) { rw_close(0,rw); }
169 const IdDataSpec cdbtcl_rwdatabases= {
170   "cdb-rwdb", "cdb-openrwdatabases-table", destroy_cdbrw_idtabcb
171 };
172
173 /*---------- File handling ----------*/
174
175 static int acquire_lock(Tcl_Interp *ip, Pathbuf *pb, int *lockfd_r) {
176   /* *lockfd_r must be -1 on entry.  If may be set to >=0 even
177    * on error, and must be closed by the caller. */
178   mode_t um, lockmode;
179   struct flock fl;
180   int r;
181
182   um= umask(~(mode_t)0);
183   umask(um);
184
185   lockmode= 0666 & ~((um & 0444)>>1);
186   /* Remove r where umask would remove w;
187    * eg umask intending 0664 here gives 0660 */
188   
189   *lockfd_r= open(pathbuf_sfx(pb,".lock"), O_RDONLY|O_CREAT, lockmode);
190   if (*lockfd_r < 0)
191     return cht_posixerr(ip, errno, "could not open/create lockfile");
192
193   fl.l_type= F_WRLCK;
194   fl.l_whence= SEEK_SET;
195   fl.l_start= 0;
196   fl.l_len= 0;
197   fl.l_pid= getpid();
198
199   r= fcntl(*lockfd_r, F_SETLK, &fl);
200   if (r == -1) {
201     if (errno == EACCES || errno == EAGAIN)
202       return cht_staticerr(ip, "lock held by another process", "CDB LOCKED");
203     else return cht_posixerr(ip, errno, "unexpected error from fcntl while"
204                              " acquiring lock");
205   }
206
207   return TCL_OK;
208 }
209
210 /*---------- Log reading and writing ----------*/
211
212 static int readlognum(FILE *f, int delim, int *num_r) {
213   int c;
214   char numbuf[20], *p, *ep;
215   unsigned long ul;
216
217   p= numbuf;
218   for (;;) {
219     c= getc(f);  if (c==EOF) return -2;
220     if (c == delim) break;
221     if (!isdigit((unsigned char)c)) return -2;
222     *p++= c;
223     if (p == numbuf+sizeof(numbuf)) return -2;
224   }
225   if (p == numbuf) return -2;
226   *p= 0;
227
228   errno=0; ul= strtoul(numbuf, &ep, 10);
229   if (*ep || errno || ul >= INT_MAX/2) return -2;
230   *num_r= ul;
231   return 0;
232 }
233
234 static int readstorelogrecord(FILE *f, HashTable *ht,
235                               int (*omitfn)(const HashValue*,
236                                             struct ht_forall_ctx *ctx),
237                               struct ht_forall_ctx *ctx,
238                               void (*updatefn)(HashTable*, const char*,
239                                                HashValue*)) {
240   /* returns:
241    *      0     for OK
242    *     -1     eof
243    *     -2     corrupt or error
244    *     -3     got newline indicating end
245    *     >0     value from omitfn
246    */
247   int keylen, vallen;
248   char *key;
249   HashValue *val;
250   int c, rc, r;
251
252   c= getc(f);
253   if (c==EOF) { if (feof(f)) return -1; return -2; }
254   if (c=='\n') return -3;
255   if (c!='+') return -2;
256
257   rc= readlognum(f, ',', &keylen);  if (rc) return rc;
258   rc= readlognum(f, ':', &vallen);  if (rc) return rc;
259
260   key= TALLOC(keylen+1);
261   val= htv_prep(vallen);
262
263   r= fread(key, 1,keylen, f);
264   if (r!=keylen) goto x2_free_keyval;
265   if (memchr(key,0,keylen)) goto x2_free_keyval;
266   key[keylen]= 0;
267
268   c= getc(f);  if (c!='-') goto x2_free_keyval;
269   c= getc(f);  if (c!='>') goto x2_free_keyval;
270   
271   r= fread(htv_fillptr(val), 1,vallen, f);
272   if (r!=vallen) goto x2_free_keyval;
273
274   rc= omitfn ? omitfn(val, ctx) : TCL_OK;
275   if (rc) { assert(rc>0); TFREE(val); }
276   else updatefn(ht, key, val);
277   
278   TFREE(key);
279   return rc;
280
281  x2_free_keyval:
282   TFREE(val);
283   TFREE(key);
284   return -2;
285 }
286
287 static int writerecord(FILE *f, const char *key, const HashValue *val) {
288   int r;
289
290   r= fprintf(f, "+%d,%d:%s->", strlen(key), val->len, key);
291   if (r<0) return -1;
292   
293   r= fwrite(val->data, 1, val->len, f);
294   if (r != val->len) return -1;
295
296   return 0;
297 }
298
299 /*---------- Creating ----------*/
300
301 int cht_do_cdbwr_create_empty(ClientData cd, Tcl_Interp *ip,
302                               const char *pathb) {
303   static const char *const toremoves[]= {
304     ".main", ".cdb", ".log", ".tmp", 0
305   };
306
307   Pathbuf pb;
308   int lock_fd=-1, fd=-1, rc, r;
309   const char *const *toremove;
310
311   pathbuf_init(&pb, pathb);
312   rc= acquire_lock(ip, &pb, &lock_fd);  if (rc) goto x_rc;
313   
314   fd= open(pathbuf_sfx(&pb, ".main"), O_RDWR|O_CREAT|O_EXCL, 0666);
315   if (fd <= 0) PE("create new database file");
316
317   for (toremove=toremoves; *toremove; toremove++) {
318     r= remove(*toremove);
319     if (r && errno != ENOENT)
320       PE("delete possible spurious file during creation");
321   }
322   
323   rc= TCL_OK;
324
325  x_rc:
326   maybe_close(fd);
327   maybe_close(lock_fd);
328   pathbuf_free(&pb);
329   return rc;
330 }
331
332 /*---------- Info callbacks ----------*/
333
334 static int infocbv3(Tcl_Interp *ip, Rw *rw, const char *arg1,
335                     const char *arg2fmt, const char *arg3, va_list al) {
336   Tcl_Obj *aa[3];
337   int na;
338   char buf[200];
339   vsnprintf(buf, sizeof(buf), arg2fmt, al);
340
341   na= 0;
342   aa[na++]= cht_ret_string(ip, arg1);
343   aa[na++]= cht_ret_string(ip, buf);
344   if (arg3) aa[na++]= cht_ret_string(ip, arg3);
345   
346   return cht_scriptinv_invoke_fg(&rw->on_info, na, aa);
347 }
348   
349 static int infocb3(Tcl_Interp *ip, Rw *rw, const char *arg1,
350                    const char *arg2fmt, const char *arg3, ...) {
351   int rc;
352   va_list al;
353   va_start(al, arg3);
354   rc= infocbv3(ip,rw,arg1,arg2fmt,arg3,al);
355   va_end(al);
356   return rc;
357 }
358   
359 static int infocb(Tcl_Interp *ip, Rw *rw, const char *arg1,
360                   const char *arg2fmt, ...) {
361   int rc;
362   va_list al;
363   va_start(al, arg2fmt);
364   rc= infocbv3(ip,rw,arg1,arg2fmt,0,al);
365   va_end(al);
366   return rc;
367 }
368   
369 /*---------- Opening ----------*/
370
371 static int cdbinit(Tcl_Interp *ip, Rw *rw) {
372   /* On entry, cdb_fd >=0 but cdb is _undefined_/
373    * On exit, either cdb_fd<0 or cdb is initialised */
374   int r, rc;
375   
376   r= cdb_init(&rw->cdb, rw->cdb_fd);
377   if (r) {
378     rc= cht_posixerr(ip, errno, "failed to initialise cdb reader");
379     close(rw->cdb_fd);  rw->cdb_fd= -1;  return rc;
380   }
381   return TCL_OK;
382 }
383
384 int cht_do_cdbwr_open(ClientData cd, Tcl_Interp *ip, const char *pathb,
385                       Tcl_Obj *on_info, Tcl_Obj *on_lexminval,
386                       void **result) {
387   const Cdbwr_SubCommand *subcmd= cd;
388   int r, rc, mainfd=-1;
389   Rw *rw;
390   struct stat stab;
391   off_t logrecstart, logjunkpos;
392
393   rw= TALLOC(sizeof(*rw));
394   ht_setup(&rw->logincore);
395   cht_scriptinv_init(&rw->on_info);
396   cht_scriptinv_init(&rw->on_lexminval);
397   rw->cdb_fd= rw->lock_fd= -1;  rw->logfile= 0;
398   pathbuf_init(&rw->pbsome, pathb);
399   pathbuf_init(&rw->pbother, pathb);
400   rw->autocompact= 1;
401
402   if (on_lexminval) {
403     rc= cht_scriptinv_set(&rw->on_lexminval, ip, on_lexminval, 0);
404     if (rc) goto x_rc;
405   } else {
406     rw->on_lexminval.llength= 0;
407   }
408
409   mainfd= open(pathbuf_sfx(&rw->pbsome,".main"), O_RDONLY);
410   if (mainfd<0) PE("open exist3ing database file .main");
411   rc= acquire_lock(ip, &rw->pbsome, &rw->lock_fd);  if (rc) goto x_rc;
412
413   r= fstat(mainfd, &stab);  if (r) PE("fstat .main");
414   rw->mainsz= stab.st_size;
415
416   rw->cdb_fd= open(pathbuf_sfx(&rw->pbsome,".cdb"), O_RDONLY);
417   if (rw->cdb_fd >=0) {
418     rc= cdbinit(ip, rw);  if (rc) goto x_rc;
419   } else if (errno == ENOENT) {
420     if (rw->mainsz) {
421       rc= cht_staticerr(ip, ".cdb does not exist but .main is nonempty -"
422                         " .cdb must have been accidentally deleted!",
423                         "CDB CDBMISSING");
424       goto x_rc;
425     }
426     /* fine */
427   } else {
428     PE("open .cdb");
429   }
430
431   rw->logfile= fopen(pathbuf_sfx(&rw->pbsome,".log"), "r+");
432   if (!rw->logfile) {
433     if (errno != ENOENT) PE("failed to open .log during open");
434     rw->logfile= fopen(rw->pbsome.buf, "w");
435     if (!rw->logfile) PE("create .log during (clean) open");
436   } else { /* rw->logfile */
437     r= fstat(fileno(rw->logfile), &stab);
438     if (r==-1) PE("fstat .log during open");
439     rc= infocb(ip, rw, "open-dirty-start", "log=%luby",
440                (unsigned long)stab.st_size);
441     if (rc) goto x_rc;
442
443     for (;;) {
444       logrecstart= ftello(rw->logfile);
445       if (logrecstart < 0) PE("ftello .log during (dirty) open");
446       r= readstorelogrecord(rw->logfile, &rw->logincore, 0,0, ht_update);
447       if (ferror(rw->logfile)) {
448         rc= cht_posixerr(ip, errno, "error reading .log during (dirty) open");
449         goto x_rc;
450       }
451       if (r==-1) {
452         break;
453       } else if (r==-2 || r==-3) {
454         char buf[100];
455         logjunkpos= ftello(rw->logfile);
456         if(logjunkpos<0) PE("ftello .log during report of junk in dirty open");
457
458         snprintf(buf,sizeof(buf), "CDB SYNTAX LOG %lu %lu",
459                  (unsigned long)logjunkpos, (unsigned long)logrecstart);
460
461         if (!(subcmd->flags & RWSCF_OKJUNK)) {
462           Tcl_SetObjErrorCode(ip, Tcl_NewStringObj(buf,-1));
463           snprintf(buf,sizeof(buf),"%lu",(unsigned long)logjunkpos);
464           Tcl_ResetResult(ip);
465           Tcl_AppendResult(ip, "syntax error (junk) in .log during"
466                            " (dirty) open, at file position ", buf, (char*)0);
467           rc= TCL_ERROR;
468           goto x_rc;
469         }
470         rc= infocb3(ip, rw, "open-dirty-junk", "errorfpos=%luby", buf,
471                     (unsigned long)logjunkpos);
472         if (rc) goto x_rc;
473
474         r= fseeko(rw->logfile, logrecstart, SEEK_SET);
475         if (r) PE("failed to fseeko .log before junk during dirty open");
476
477         r= ftruncate(fileno(rw->logfile), logrecstart);
478         if (r) PE("ftruncate .log to chop junk during dirty open");
479       } else {
480         assert(!r);
481       }
482     }
483   }
484   /* now log is positioned for appending and everything is read */
485
486   *result= rw;
487   maybe_close(mainfd);
488   return TCL_OK;
489
490  x_rc:
491   rw_close(0,rw);
492   maybe_close(mainfd);
493   return rc;
494 }
495
496 /*==================== COMPACTION ====================*/
497
498 struct ht_forall_ctx {
499   struct cdb_make cdbm;
500   FILE *mainfile;
501   int lexminvall;
502   long *reccount;
503   const char *lexminval;
504 };
505
506 /*---------- helper functions ----------*/
507
508 static int expiredp(const HashValue *val, struct ht_forall_ctx *a) {
509   int r, l;
510   if (!val->len) return 0;
511   l= val->len < a->lexminvall ? val->len : a->lexminvall;
512   r= memcmp(val->data, a->lexminval, l);
513   if (r>0) return 0;
514   if (r<0) return 1;
515   return val->len < a->lexminvall;
516 }
517
518 static int delete_ifexpired(const char *key, HashValue *val,
519                             struct ht_forall_ctx *a) {
520   if (!expiredp(val, a)) return 0;
521   val->len= 0;
522   /* we don't actually need to realloc it to free the memory because
523    * this will shortly all be deleted as part of the compaction */
524   return 0;
525 }
526
527 static int addto_cdb(const char *key, HashValue *val,
528                      struct ht_forall_ctx *a) {
529   return cdb_make_add(&a->cdbm, key, strlen(key), val->data, val->len);
530 }
531
532 static int addto_main(const char *key, HashValue *val,
533                       struct ht_forall_ctx *a) {
534   (*a->reccount)++;
535   return writerecord(a->mainfile, key, val);
536 }
537
538 /*---------- compact main entrypoint ----------*/
539
540 static int compact_core(Tcl_Interp *ip, Rw *rw, unsigned long logsz,
541                         long *reccount_r) {
542   /* creates new .cdb and .main
543    * closes logfile
544    * leaves .log with old data
545    * leaves cdb fd open onto old db
546    * leaves logincore full of crap
547    */
548   int r, rc;
549   int cdbfd, cdbmaking;
550   off_t errpos, newmainsz;
551   char buf[100];
552   Tcl_Obj *res;
553   struct ht_forall_ctx a;
554
555   a.mainfile= 0;
556   cdbfd= -1;
557   cdbmaking= 0;
558   *reccount_r= 0;
559   a.reccount= reccount_r;
560
561   r= fclose(rw->logfile);
562   if (r) { rc= cht_posixerr(ip, errno, "probable data loss!  failed to fclose"
563                             " logfile during compact");  goto x_rc; }
564   rw->logfile= 0;
565   
566   rc= infocb(ip, rw, "compact-start", "log=%luby main=%luby",
567              logsz, (unsigned long)rw->mainsz);
568   if (rc) goto x_rc;
569
570   if (rw->on_lexminval.llength) {
571     rc= cht_scriptinv_invoke_fg(&rw->on_lexminval, 0,0);
572     if (rc) goto x_rc;
573
574     res= Tcl_GetObjResult(ip);  assert(res);
575     a.lexminval= Tcl_GetStringFromObj(res, &a.lexminvall);
576     assert(a.lexminval);
577
578     /* we rely not calling Tcl_Eval during the actual compaction;
579      * if we did Tcl_Eval then the interp result would be trashed.
580      */
581     rc= ht_forall(&rw->logincore, delete_ifexpired, &a);
582
583   } else {
584     a.lexminval= "";
585   }
586
587   /* merge unsuperseded records from main into hash table */
588
589   a.mainfile= fopen(pathbuf_sfx(&rw->pbsome,".main"), "r");
590   if (!a.mainfile) PE("failed to open .main for reading during compact");
591
592   for (;;) {
593     r= readstorelogrecord(a.mainfile, &rw->logincore,
594                           expiredp, &a,
595                           ht_maybeupdate);
596     if (ferror(a.mainfile)) { rc= cht_posixerr(ip, errno, "error reading"
597                          " .main during compact"); goto x_rc;
598     }
599     if (r==-3) {
600       break;
601     } else if (r==-1 || r==-2) {
602       errpos= ftello(a.mainfile);
603       if (errpos<0) PE("ftello .main during report of syntax error");
604       snprintf(buf,sizeof(buf), "CDB SYNTAX MAIN %lu", (unsigned long)errpos);
605       Tcl_SetObjErrorCode(ip, Tcl_NewStringObj(buf,-1));
606       snprintf(buf,sizeof(buf), "%lu", (unsigned long)errpos);
607       Tcl_ResetResult(ip);
608       Tcl_AppendResult(ip, "syntax error in .main during"
609                        " compact, at file position ", buf, (char*)0);
610       rc= TCL_ERROR;
611       goto x_rc;
612     } else {
613       assert(!rc);
614     }
615   }
616   fclose(a.mainfile);
617   a.mainfile= 0;
618
619   /* create new cdb */
620
621   cdbfd= open(pathbuf_sfx(&rw->pbsome,".tmp"), O_WRONLY|O_CREAT|O_TRUNC, 0666);
622   if (cdbfd<0) PE("create .tmp for new cdb during compact");
623
624   r= cdb_make_start(&a.cdbm, cdbfd);
625   if (r) PE("cdb_make_start during compact");
626   cdbmaking= 1;
627
628   r= ht_forall(&rw->logincore, addto_cdb, &a);
629   if (r) PE("cdb_make_add during compact");
630
631   r= cdb_make_finish(&a.cdbm);
632   if(r) PE("cdb_make_finish during compact");
633   cdbmaking= 0;
634
635   r= fdatasync(cdbfd);  if (r) PE("fdatasync new cdb during compact");
636   r= close(cdbfd);  if (r) PE("close new cdb during compact");
637   cdbfd= -1;
638
639   r= rename(rw->pbsome.buf, pathbuf_sfx(&rw->pbother,".cdb"));
640   if (r) PE("install new .cdb during compact");
641
642   /* create new main */
643
644   a.mainfile= fopen(pathbuf_sfx(&rw->pbsome,".tmp"), "w");
645   if (!a.mainfile) PE("create .tmp for new main during compact");
646
647   r= ht_forall(&rw->logincore, addto_main, &a);
648   if (r) { rc= cht_posixerr(ip, r, "error writing to new .main"
649                             " during compact");  goto x_rc; }
650   
651   r= fflush(a.mainfile);  if (r) PE("fflush new main during compact");
652   r= fdatasync(fileno(a.mainfile));
653   if (r) PE("fdatasync new main during compact");
654
655   newmainsz= ftello(a.mainfile);
656   if (newmainsz<0) PE("ftello new main during compact");
657   
658   r= fclose(a.mainfile);  if (r) PE("fclose new main during compact");
659   a.mainfile= 0;
660
661   r= rename(rw->pbsome.buf, pathbuf_sfx(&rw->pbother,".main"));
662   if (r) PE("install new .main during compact");
663
664   rw->mainsz= newmainsz;
665
666   /* done! */
667   
668   rc= infocb(ip, rw, "compact-end", "main=%luby nrecs=%l",
669              (unsigned long)rw->mainsz, *a.reccount);
670   if (rc) goto x_rc;
671
672   return rc;
673
674 x_rc:
675   if (a.mainfile) fclose(a.mainfile);
676   if (cdbmaking) cdb_make_finish(&a.cdbm);
677   maybe_close(cdbfd);
678   remove(pathbuf_sfx(&rw->pbsome,".tmp")); /* for tidyness */
679   return rc;
680 }
681
682 /*---------- Closing ----------*/
683
684 static int compact_forclose(Tcl_Interp *ip, Rw *rw, long *reccount_r) {
685   off_t logsz;
686   int r, rc;
687
688   logsz= ftello(rw->logfile);
689   if (logsz < 0) PE("ftello logfile (during tidy close)");
690
691   rc= compact_core(ip, rw, logsz, reccount_r);  if (rc) goto x_rc;
692
693   r= remove(pathbuf_sfx(&rw->pbsome,".log"));
694   if (r) PE("remove .log (during tidy close)");
695
696   return TCL_OK;
697
698 x_rc: return rc;
699 }
700   
701 int cht_do_cdbwr_close(ClientData cd, Tcl_Interp *ip, void *rw_v) {
702   Rw *rw= rw_v;
703   int rc, rc_close;
704   long reccount= -1;
705   off_t logsz;
706
707   if (rw->autocompact) rc= compact_forclose(ip, rw, &reccount);
708   else rc= TCL_OK;
709
710   if (!rc) {
711     if (!rw->logfile) {
712       logsz= ftello(rw->logfile);
713       if (logsz < 0)
714         rc= cht_posixerr(ip, errno, "ftell logfile during close info");
715       else
716         rc= infocb(ip, rw, "close", "main=%luby log=%luby",
717                    rw->mainsz, logsz);
718     } else if (reccount>=0) {
719       rc= infocb(ip, rw, "close", "main=%luby nrecs=%l", rw->mainsz, reccount);
720     } else {
721       rc= infocb(ip, rw, "close", "main=%luby", rw->mainsz);
722     }
723   }
724   rc_close= rw_close(ip,rw);
725   if (rc_close) rc= rc_close;
726   
727   cht_tabledataid_disposing(ip, rw_v, &cdbtcl_rwdatabases);
728   return rc;
729 }
730
731 /*---------- Other compaction-related entrypoints ----------*/
732
733 static int compact_keepopen(Tcl_Interp *ip, Rw *rw, int force) {
734   off_t logsz;
735   long reccount;
736   int rc, r;
737
738   logsz= ftello(rw->logfile);
739   if (logsz < 0) return cht_posixerr(ip, errno, "ftell .log"
740                                        " during compact check or force");
741
742   if (!force && logsz < rw->mainsz / 10 + 1000) return TCL_OK;
743
744   rc= compact_core(ip, rw, logsz, &reccount);  if (rc) goto x_rc;
745
746   maybe_close(rw->cdb_fd);
747   rw->cdb_fd= -1;
748   ht_destroy(&rw->logincore);
749   ht_setup(&rw->logincore);
750
751   rw->cdb_fd= open(pathbuf_sfx(&rw->pbsome,".cdb"), O_RDONLY);
752   if (rw->cdb_fd < 0) PE("reopen .cdb after compact");
753
754   rc= cdbinit(ip, rw);  if (rc) goto x_rc;
755
756   rw->logfile= fopen(pathbuf_sfx(&rw->pbsome,".log"), "w");
757   if (!rw->logfile) PE("reopen .log after compact");
758
759   r= fsync(fileno(rw->logfile));  if (r) PE("fsync .log after compact reopen");
760
761   return TCL_OK;
762
763 x_rc:
764   /* doom! all updates fail after this (because rw->logfile is 0), and
765    * we may be using a lot more RAM than would be ideal.  Program will
766    * have to reopen if it really wants sanity. */
767   return rc;
768 }
769
770 int cht_do_cdbwr_compact_force(ClientData cd, Tcl_Interp *ip, void *rw_v) {
771   return compact_keepopen(ip, rw_v, 1);
772 }
773 int cht_do_cdbwr_compact_check(ClientData cd, Tcl_Interp *ip, void *rw_v) {
774   return compact_keepopen(ip, rw_v, 0);
775 }
776
777 int cht_do_cdbwr_compact_explicit(ClientData cd, Tcl_Interp *ip, void *rw_v) {
778   Rw *rw= rw_v;
779   rw->autocompact= 0;
780   return TCL_OK;
781 }
782 int cht_do_cdbwr_compact_auto(ClientData cd, Tcl_Interp *ip, void *rw_v) {
783   Rw *rw= rw_v;
784   rw->autocompact= 1;
785   return TCL_OK;
786 }
787
788 /*---------- Updateing ----------*/
789
790 static int update(Tcl_Interp *ip, Rw *rw, const char *key,
791                   const Byte *data, int dlen) {
792   HashValue *val;
793   int rc, r;
794
795   if (!rw->logfile) return cht_staticerr
796     (ip, "previous compact failed; cdbwr must be closed and reopened "
797      "before any further updates", "CDB BROKEN");
798   
799   val= htv_prep(dlen);  assert(val);
800   memcpy(htv_fillptr(val), data, dlen);
801
802   r= writerecord(rw->logfile, key, val);
803   if (!r) r= fflush(rw->logfile);
804   if (r) PE("write update to logfile");
805
806   ht_update(&rw->logincore, key, val);
807   return compact_keepopen(ip, rw, 0);
808
809  x_rc:
810   TFREE(val);
811   return rc;
812 }  
813
814 int cht_do_cdbwr_update(ClientData cd, Tcl_Interp *ip,
815                         void *rw_v, const char *key, Tcl_Obj *value) {
816   int dlen;
817   const char *data;
818   data= Tcl_GetStringFromObj(value, &dlen);  assert(data);
819   return update(ip, rw_v, key, data, dlen);
820 }
821
822 int cht_do_cdbwr_update_hb(ClientData cd, Tcl_Interp *ip,
823                            void *rw_v, const char *key, HBytes_Value value) {
824   return update(ip, rw_v, key, cht_hb_data(&value), cht_hb_len(&value));
825 }