git: plan9front

Download patch

ref: c53182c3ef7d2360eb9de535e217e1d30fb15002
parent: 831c8b619cf7731d747a9b0f6c3d2548a28b74ea
author: Ori Bernstein <ori@eigenstate.org>
date: Sat Mar 8 18:13:29 EST 2025

gefs: allow custom snapshot schedules

--- a/sys/man/8/gefs
+++ b/sys/man/8/gefs
@@ -22,9 +22,22 @@
 .B save trace
 .I filename
 .PP
+.B set
+[
+.I snap
+]
+.I key
+.I val
+.PP
+.B clr
+[
+.I snap
+]
+.I key
+.PP
 .B snap
 [
--Smdl
+-mdl
 ]
 [
 .I old 
@@ -83,6 +96,46 @@
 to modify the owner of the file.
 This may be useful during file system initialization.
 .PP
+.I Set
+or
+.I clr
+sets or clears a configuration key, respectively.
+If the
+.I snap
+name is passed, then the key is set in that snapshot.
+If not, then it is set globally.
+This key is a free form string which controls
+some aspect of file system behavior on that snapshot.
+All changes take effect after the file system is restarted.
+Currently, one configuration option is supported:
+.TP
+.BI retain 'timespec'
+sets the schedule for which snapshots are retained.
+The timespec is a sequence of counts and intervals,
+in the form <
+.BR count >@<
+.BR interval >< 
+.BR scale >.
+Every
+.BR interval ,
+a new snapshot is taken.
+Up to
+.B count
+snapshots are retained in the history, with old ones aging out.
+If the 
+.B count
+is omitted, then old snapshots are not trimmed.
+If the
+.B interval
+is omitted, it defaults to 1.
+.IP
+Snapshot timing is taken from the config key attached to the snapshot.
+If there is no key attached to the snapshot,
+the global config key is used.
+If there is no global config key, the default value
+.B `60@1m 24@1h @1d'
+is used.
+.PP
 .B Snap
 manages snapshots.
 It can be invoked as
@@ -123,10 +176,6 @@
 This flag only has an effect when tagging a new snapshot,
 and is ignored otherwise.
 .TP
-.B -S
-Disables automatic timed snapshots.
-This flag only has an effect when tagging a new snapshot,
-and is ignored otherwise.
 .PP
 .I Sync
 writes dirty blocks in memory to the disk.
@@ -239,7 +288,8 @@
 checkpoints:
 .IP
 .EX
-gefs# snap -Sm main mymutable
+gefs# snap -m main mymutable
+gefs# set mymutable retain ''
 .EE
 .PP
 To delete a snapshot:
@@ -246,6 +296,22 @@
 .IP
 .EX
 gefs# snap -d mysnap
+.EE
+.PP
+To set the snapshot retention on the main snapshot
+to 12 snapshots taken every five minutes,
+along with 48 snapshots taken every 2 hours,
+and a daily snapshot with no trimming:
+.IP
+.EX
+gefs# set main retain '12@5m 48@2h @d'
+.EE
+.PP
+To set the default snapshot retention to daily,
+with no minute or hour snapshots:
+.IP
+.EX
+gefs# set retain @d
 .EE
 .PP
 To create a new user:
--- a/sys/src/cmd/gefs/cons.c
+++ b/sys/src/cmd/gefs/cons.c
@@ -67,16 +67,8 @@
 			break;
 		flg = UNPACK32(s.kv.v+1+8);
 		fprint(fd, "snap %.*s", s.kv.nk-1, s.kv.k+1);
-		if(flg != 0)
-			fprint(fd, " [");
 		if(flg & Lmut)
-			fprint(fd, " mutable");
-		if(flg & Lauto)
-			fprint(fd, " auto");
-		if(flg & Ltsnap)
-			fprint(fd, " tsnap");
-		if(flg != 0)
-			fprint(fd, " ]");
+			fprint(fd, " [mutable]");
 		fprint(fd, "\n");
 	}
 	btexit(&s);
@@ -88,17 +80,12 @@
 	Amsg *a;
 	int i;
 
-	if((a = mallocz(sizeof(Amsg), 1)) == nil){
-		fprint(fd, "alloc sync msg: %r\n");
-		return;
-	}
+	a = emalloc(sizeof(Amsg), 1);
 	a->op = AOsnap;
 	a->fd = fd;
-	a->flag = Ltsnap;
 	while(ap[0][0] == '-'){
 		for(i = 1; ap[0][i]; i++){
 			switch(ap[0][i]){
-			case 'S':	a->flag &= ~Ltsnap;	break;
 			case 'm':	a->flag |= Lmut;	break;
 			case 'd':	a->delete++;		break;
 			case 'l':
@@ -364,11 +351,42 @@
 }
 
 static void
+setcfg(int fd, char **ap, int n)
+{
+	Amsg *a;
+
+	a = emalloc(sizeof(Amsg), 1);
+	a->op = AOsetcfg;
+	a->fd = fd;
+	if(n == 3)
+		strecpy(a->snap, a->snap+sizeof(a->snap), *ap++);
+	strecpy(a->key, a->key+sizeof(a->key), *ap++);
+	strecpy(a->val, a->val+sizeof(a->val), *ap);
+	chsend(fs->admchan, a);
+}
+
+static void
+clrcfg(int fd, char **ap, int n)
+{
+	Amsg *a;
+
+	a = emalloc(sizeof(Amsg), 1);
+	a->op = AOclrcfg;
+	a->fd = fd;
+	if(n == 2)
+		strecpy(a->snap, a->snap+sizeof(a->snap), *ap++);
+	strecpy(a->key, a->key+sizeof(a->key), *ap);
+	chsend(fs->admchan, a);
+}
+
+static void
 help(int fd, char**, int)
 {
 	char *msg =
 		"help -- show this help\n"
 		"check -- check for consistency\n"
+		"set [snap] key val -- set configuration key\n"
+		"clear [snap] key -- clear configuration key\n"
 		"df -- show disk usage\n"
 		"halt -- stop all writers, sync, and go read-only\n"
 		"permit [on|off] -- switch to/from permissive mode\n"
@@ -387,6 +405,8 @@
 Cmd cmdtab[] = {
 	/* admin */
 	{.name="check",		.sub=nil,	.minarg=0, .maxarg=0, .fn=fsckfs, .epoch=1},
+	{.name="set",		.sub=nil,	.minarg=2, .maxarg=3, .fn=setcfg},
+	{.name="clear",		.sub=nil,	.minarg=1, .maxarg=2, .fn=clrcfg},
 	{.name="df",		.sub=nil, 	.minarg=0, .maxarg=0, .fn=showdf},
 	{.name="halt",		.sub=nil,	.minarg=0, .maxarg=0, .fn=haltfs},
 	{.name="help",		.sub=nil,	.minarg=0, .maxarg=0, .fn=help},
--- a/sys/src/cmd/gefs/dat.h
+++ b/sys/src/cmd/gefs/dat.h
@@ -1,32 +1,33 @@
-typedef struct Blk	Blk;
 typedef struct Amsg	Amsg;
-typedef struct Gefs	Gefs;
+typedef struct Arange	Arange;
+typedef struct Arena	Arena;
+typedef struct Bfree	Bfree;
+typedef struct Blk	Blk;
+typedef struct Bptr	Bptr;
+typedef struct Bucket	Bucket;
+typedef struct Chan	Chan;
+typedef struct Conn	Conn;
+typedef struct Cron	Cron;
+typedef struct Dent	Dent;
+typedef struct Dlist	Dlist;
 typedef struct Errctx	Errctx;
-typedef struct Fmsg	Fmsg;
 typedef struct Fid	Fid;
-typedef struct Msg	Msg;
+typedef struct Fmsg	Fmsg;
+typedef struct Gefs	Gefs;
 typedef struct Key	Key;
-typedef struct Val	Val;
 typedef struct Kvp	Kvp;
-typedef struct Xdir	Xdir;
-typedef struct Bptr	Bptr;
 typedef struct Limbo	Limbo;
-typedef struct Bfree	Bfree;
+typedef struct Mount	Mount;
+typedef struct Msg	Msg;
+typedef struct Qent	Qent;
 typedef struct Scan	Scan;
-typedef struct Dent	Dent;
 typedef struct Scanp	Scanp;
-typedef struct Arena	Arena;
-typedef struct Arange	Arange;
-typedef struct Bucket	Bucket;
-typedef struct Chan	Chan;
 typedef struct Syncq	Syncq;
-typedef struct Qent	Qent;
 typedef struct Trace	Trace;
 typedef struct Tree	Tree;
-typedef struct Dlist	Dlist;
-typedef struct Mount	Mount;
 typedef struct User	User;
-typedef struct Conn	Conn;
+typedef struct Val	Val;
+typedef struct Xdir	Xdir;
 
 enum {
 	KiB	= 1024ULL,
@@ -107,6 +108,9 @@
 	Klabel,	/* name[] => snapid[]:			snapshot label */
 	Ksnap,	/* sid[8] => ref[8], tree[52]:		snapshot root */
 	Kdlist,	/* snap[8] gen[8] => hd[ptr],tl[ptr]	deadlist  */
+
+	/* configuration */
+	Kconf,	/* name[] => value[] */
 };
 
 enum {
@@ -121,12 +125,13 @@
 
 enum {
 	Lmut	= 1 << 0,	/* can we modify snaps via this label */
-	Lauto	= 1 << 1,	/* was this label generated automatically */
-	Ltsnap	= 1 << 2,	/* should we skip the timed snapshots */
+	_Lauto	= 1 << 1,	/* deprecated: was auto snap */
+	_Ltsnap	= 1 << 2,	/* deprecated: was skipping timed snaps */
 };
 
 enum {
-	Qdump = 1ULL << 63,
+	Qdump	= 1ULL << 63,
+	Qctl	= ~0ULL,
 };
 
 #define Zb (Bptr){-1, -1, -1}
@@ -326,6 +331,8 @@
 	AOhalt,
 	AOclear,
 	AOrclose,
+	AOsetcfg,
+	AOclrcfg,
 };
 
 enum {
@@ -405,8 +412,12 @@
 			char	new[128];
 			int	flag;
 			char	delete;
-
 		};
+		struct { /* AOsetcfg AOclrcfg */
+			char	snap[128];
+			char	key[32];
+			char	val[128];
+		};
 		struct {	/* AOclear, AOrclose */
 			Mount	*mnt;
 			Dent	*dent;
@@ -635,6 +646,15 @@
 	};
 };
 
+struct Cron {
+	int	i;
+	int	cnt;
+	vlong	div;
+	vlong	last;
+	char	*tag;
+	char	(*lbl)[128];
+};
+
 struct Mount {
 	Limbo;
 	Lock;
@@ -643,7 +663,6 @@
 	vlong	gen;
 	char	name[64];
 	Tree	*root;	/* EBR protected */
-
 	int	flag;
 
 	/* open directory entries */
@@ -650,9 +669,8 @@
 	Lock	dtablk;
 	Dent	*dtab[Ndtab];
 
-	/* snapshot history */
-	char	minutely[60][128];
-	char	hourly[24][128];
+	/* snapshot scheduling */
+	Cron	cron[3];
 };
 
 struct Conn {
--- a/sys/src/cmd/gefs/dump.c
+++ b/sys/src/cmd/gefs/dump.c
@@ -43,6 +43,9 @@
 		n = fmtprint(fmt, "dlist gen:%lld, bgen:%lld",
 			UNPACK64(k->k+1), UNPACK64(k->k+9));
 		break;
+	case Kconf:
+		n = fmtprint(fmt, "Conf %.*s", k->nk-1, k->k+1);
+		break;
 	default:
 		n = fmtprint(fmt, "??? %.*H", k->nk, k->k);
 		break;
@@ -165,6 +168,9 @@
 		n = fmtprint(fmt, "hd:%B, tl:%B",
 			unpackbp(v->v, v->nv),
 			unpackbp(v->v+Ptrsz, v->nv-Ptrsz));
+		break;
+	case Kconf:
+		n = fmtprint(fmt, "%.*s", v->nv, v->v);
 		break;
 	default:
 		n = fmtprint(fmt, "??? %.*H", v->nk, v->k);
--- a/sys/src/cmd/gefs/fs.c
+++ b/sys/src/cmd/gefs/fs.c
@@ -11,7 +11,7 @@
 static void	respond(Fmsg*, Fcall*);
 static void	rerror(Fmsg*, char*, ...);
 static void	clunkfid(Conn*, Fid*, Amsg**);
-
+static void	snapmsg(char*, char*);
 static void	authfree(AuthRpc*);
 
 int
@@ -573,49 +573,98 @@
 	return de;
 }
 
-static void
-loadautos(Mount *mnt)
+/*
+ * Load snapshots into history, trimming old ones if we have more
+ * snapshots than expected (eg, reducing the count in the config).
+ */
+void
+loadhist(Mount *mnt, Cron *c)
 {
 	char pfx[128];
-	int m, h, ns;
-	uint flg;
+	int i, ns;
 	Scan s;
 
-	m = 0;
-	h = 0;
+	if(c->cnt == 0)
+		return;
+	i = 0;
 	pfx[0] = Klabel;
-	ns = snprint(pfx+1, sizeof(pfx)-1, "%s@minute.", mnt->name);
+	ns = snprint(pfx+1, sizeof(pfx)-1, "%s@%s.", mnt->name, c->tag);
 	btnewscan(&s, pfx, ns+1);
 	btenter(&fs->snap, &s);
 	while(1){
 		if(!btnext(&s, &s.kv))
 			break;
-		flg = UNPACK32(s.kv.v+1+8);
-		if(flg & Lauto){
-			memcpy(mnt->minutely[m], s.kv.k+1, s.kv.nk-1);
-			mnt->minutely[m][s.kv.nk-1] = 0;
-			m = (m+1)%60;
-			continue;
-		}
+		if(c->lbl[i][0] != 0)
+			snapmsg(c->lbl[i], nil);
+		assert(s.kv.nk-1 < sizeof(c->lbl[i]));
+		memcpy(c->lbl[i], s.kv.k+1, s.kv.nk-1);
+		c->lbl[i][s.kv.nk-1] = 0;
+		i = (i+1) % c->cnt;
 	}
 	btexit(&s);
+	c->last = time(nil);
+	c->i = i;
+}
 
-	pfx[0] = Klabel;
-	ns = snprint(pfx+1, sizeof(pfx)-1, "%s@hour.", mnt->name);
-	btnewscan(&s, pfx, ns+1);
-	btenter(&fs->snap, &s);
-	while(1){
-		if(!btnext(&s, &s.kv))
-			break;
-		flg = UNPACK32(s.kv.v+1+8);
-		if(flg & Lauto){
-			memcpy(mnt->hourly[h], s.kv.k+1, s.kv.nk-1);
-			mnt->hourly[h][s.kv.nk-1] = 0;
-			h = (h+1)%24;
-			continue;
+/*
+ * Load up automatic snapshots. Don't error on bad config,
+ * so that the user can mount the snap and fix it.
+ */
+static void
+loadautos(Mount *mnt)
+{
+	static char *tagname[] = {"minute", "hour", "day"};
+	static int scale[] = {60, 3600, 24*3600};
+	char *p, pfx[128], rbuf[128];
+	int i, n, div, cnt, op;
+	Kvp kv;
+
+	pfx[0] = Kconf;
+	n = snprint(pfx+1, sizeof(pfx)-1, "retain");
+	kv.k = pfx;
+	kv.nk = n+1;
+	if(btlookup(mnt->root, &kv, &kv, rbuf, sizeof(rbuf)-1)
+	|| btlookup(&fs->snap, &kv, &kv, rbuf, sizeof(rbuf)-1)){
+		p[kv.nv] = 0;
+		p = kv.v;
+	}else
+		p = "60@m 24@h @d";
+	while(*p){
+		cnt = 0;
+		div = 1;
+		op = -1;
+
+		/* parse the conf. */
+		if(*p >= '0' && *p <= '9')
+			cnt = strtol(p, &p, 10);
+		if(*p++ != '@')
+			goto Bad;
+		if(*p >= '0' && *p <= '9')
+			div = strtol(p, &p, 10);
+		if(*p == 'm' || *p == 'h' || *p == 'd')
+			op = *p++;
+		while(*p == ' ' || *p == '\t')
+			p++;
+		if(cnt < 0 || div <= 0){
+Bad:			memset(mnt->cron, 0, sizeof(mnt->cron));
+			fprint(2, "invalid time spec\n");
+			return;
 		}
+
+		switch(op){
+		case 'm':	i = 0;	break;
+		case 'h':	i = 1;	break;
+		case 'd':	i = 2;	break;
+		default:	abort();
+		}
+
+		mnt->cron[i].tag = tagname[i];
+		mnt->cron[i].div = scale[i]*div;
+		mnt->cron[i].cnt = cnt;
+		mnt->cron[i].lbl = emalloc(cnt*sizeof(char[128]), 1);
 	}
-	btexit(&s);
+	for(i = 0; i < nelem(mnt->cron); i++)
+		loadhist(mnt, &mnt->cron[i]);
 }
 
 Mount *
@@ -650,10 +699,10 @@
 	snprint(mnt->name, sizeof(mnt->name), "%s", name);
 	if((t = opensnap(name, &flg)) == nil)
 		error(Enosnap);
-	loadautos(mnt);
 	mnt->flag = flg;
 	mnt->root = t;
 	mnt->next = fs->mounts;
+	loadautos(mnt);
 	asetp(&fs->mounts, mnt);
 	poperror();
 
@@ -2655,6 +2704,43 @@
 }
 
 void
+setconf(int fd, int op, char *snap, char *key, char *val)
+{
+	char kbuf[128];
+	Mount *mnt;
+	Tree *t;
+	Msg m;
+
+	mnt = nil;
+	t = &fs->snap;
+	if(waserror()){
+		fprint(fd, "error setting config: %s\n", errmsg());
+		return;
+	}
+	if(snap[0] != 0){
+		mnt = getmount(snap);
+		t = mnt->root;
+	}
+	kbuf[0] = Kconf;
+	strecpy(kbuf+1, kbuf+sizeof(kbuf), key);
+	m.op = op;
+	m.k = kbuf;
+	m.nk = strlen(key)+1;
+	m.v = val;
+	m.nv = strlen(val);
+	qlock(&fs->mutlk);
+	if(!waserror()){
+		btupsert(t, &m, 1);
+		poperror();
+	}else
+		fprint(fd, "error setting config: %s\n", errmsg());
+	qunlock(&fs->mutlk);
+	if(mnt != nil)
+		clunkmount(mnt);
+	poperror();
+}
+
+void
 runsweep(int id, void*)
 {
 	char buf[Kvmax];
@@ -2672,6 +2758,12 @@
 	while(1){
 		am = chrecv(fs->admchan);
 		switch(am->op){
+		case AOsetcfg:
+			setconf(am->fd, Oinsert, am->snap, am->key, am->val);
+			break;
+		case AOclrcfg:
+			setconf(am->fd, Odelete, am->snap, am->key, "");
+			break;
 		case AOhalt:
 			if(!agetl(&fs->rdonly)){
 				ainc(&fs->rdonly);
@@ -2871,8 +2963,8 @@
 	}
 }
 
-void
-snapmsg(char *old, char *new, int flg)
+static void
+snapmsg(char *old, char *new)
 {
 	Amsg *a;
 
@@ -2879,7 +2971,6 @@
 	a = emalloc(sizeof(Amsg), 1);
 	a->op = AOsnap;
 	a->fd = -1;
-	a->flag = flg;
 	strecpy(a->old, a->old+sizeof(a->old), old);
 	if(new == nil)
 		a->delete = 1;
@@ -2888,19 +2979,43 @@
 	chsend(fs->admchan, a);
 }
 
+static void
+cronsync(char *name, Cron *c, Tm *tm, vlong now)
+{
+	char *p, *e, buf[128];
+
+	if(c->div == 0)
+		return;
+	if(now/c->div == c->last/c->div)
+		return;
+
+	if(c->cnt == 0){
+		p = buf;
+		e = p + sizeof(buf);
+	}else{
+		if(c->lbl[c->i][0] != 0)
+			snapmsg(c->lbl[c->i], nil);
+		p = c->lbl[c->i];
+		e = p + sizeof(c->lbl[c->i]);
+		c->i = (c->i+1)%c->cnt;
+	}
+	seprint(p, e, "%s@%s.%τ",
+		name, c->tag,
+		tmfmt(tm, "YYYY.MM.DD[_]hh:mm:ss"));
+	snapmsg(name, p);
+	c->last = now;
+}
+
 void
 runtasks(int, void *)
 {
-	char buf[128];
-	Tm now, then;
+	vlong now;
 	Mount *mnt;
-	int m, h;
 	Amsg *a;
+	Tm tm;
+	int i;
 
-	m = 0;
-	h = 0;
-	tmnow(&then, nil);
-	tmnow(&now, nil);
+	tmnow(&tm, nil);
 	while(1){
 		sleep(5000);
 		if(fs->rdonly)
@@ -2914,35 +3029,14 @@
 		a->fd = -1;
 		chsend(fs->admchan, a);
 
-		tmnow(&now, nil);
+		tmnow(&tm, nil);
+		now = tmnorm(&tm);
 		for(mnt = agetp(&fs->mounts); mnt != nil; mnt = mnt->next){
-			if(!(mnt->flag & Ltsnap))
+			if(!(mnt->flag & Lmut))
 				continue;
-			if(now.yday != then.yday){
-				snprint(buf, sizeof(buf),
-					"%s@day.%τ", mnt->name, tmfmt(&now, "YYYY.MM.DD[_]hh:mm:ss"));
-				snapmsg(mnt->name, buf, Lauto);
-			}
-			if(now.hour != then.hour){
-				if(mnt->hourly[h][0] != 0)
-					snapmsg(mnt->hourly[h], nil, 0);
-				snprint(mnt->hourly[h], sizeof(mnt->hourly[h]),
-					"%s@hour.%τ", mnt->name, tmfmt(&now, "YYYY.MM.DD[_]hh:mm:ss"));
-				snapmsg(mnt->name, mnt->hourly[h], Lauto);
-			}
-			if(now.min != then.min){
-				if(mnt->minutely[m][0] != 0)
-					snapmsg(mnt->minutely[m], nil, 0);
-				snprint(mnt->minutely[m], sizeof(mnt->minutely[m]),
-					"%s@minute.%τ", mnt->name, tmfmt(&now, "YYYY.MM.DD[_]hh:mm:ss"));
-				snapmsg(mnt->name, mnt->minutely[m], Lauto);
-			}
+			for(i = 0; i < nelem(mnt->cron); i++)
+				cronsync(mnt->name, &mnt->cron[i], &tm, now);
 		}
-		if(now.hour != then.hour)
-			h = (h+1)%24;
-		if(now.min != then.min)
-			m = (m+1)%60;
-		then = now;
 		poperror();
 	}
 }
--- a/sys/src/cmd/gefs/ream.c
+++ b/sys/src/cmd/gefs/ream.c
@@ -91,11 +91,11 @@
 	Tree t;
 	Kvp kv;
 
-	lbl2kv("adm", 1, Lmut|Ltsnap, &kv, buf, sizeof(buf));
+	lbl2kv("adm", 1, Lmut, &kv, buf, sizeof(buf));
 	setval(s, &kv);
 	lbl2kv("empty", 0, 0, &kv, buf, sizeof(buf));
 	setval(s, &kv);
-	lbl2kv("main", 2, Lmut|Ltsnap, &kv, buf, sizeof(buf));
+	lbl2kv("main", 2, Lmut, &kv, buf, sizeof(buf));
 	setval(s, &kv);
 
 	p = buf;
--