Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F159727548
D19227.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
D19227.diff
View Options
Index: lib/geom/concat/geom_concat.c
===================================================================
--- lib/geom/concat/geom_concat.c
+++ lib/geom/concat/geom_concat.c
@@ -53,6 +53,13 @@
static void concat_label(struct gctl_req *req);
struct g_command class_commands[] = {
+ { "append", G_FLAG_VERBOSE, NULL,
+ {
+ { 'h', "hardcode", NULL, G_TYPE_BOOL },
+ G_OPT_SENTINEL
+ },
+ "[-hv] name prov"
+ },
{ "clear", G_FLAG_VERBOSE, concat_main, G_NULL_OPTS,
"[-v] prov ..."
},
Index: sys/geom/concat/g_concat.h
===================================================================
--- sys/geom/concat/g_concat.h
+++ sys/geom/concat/g_concat.h
@@ -69,13 +69,16 @@
} \
} while (0)
+
struct g_concat_disk {
+ TAILQ_ENTRY(g_concat_disk) d_next;
struct g_consumer *d_consumer;
struct g_concat_softc *d_softc;
off_t d_start;
off_t d_end;
int d_candelete;
int d_removed;
+ bool d_hardcoded;
};
struct g_concat_softc {
@@ -84,9 +87,11 @@
struct g_provider *sc_provider;
uint32_t sc_id; /* concat unique ID */
- struct g_concat_disk *sc_disks;
uint16_t sc_ndisks;
- struct mtx sc_lock;
+ TAILQ_HEAD(g_concat_disks, g_concat_disk) sc_disks;
+
+ struct mtx sc_completion_lock; /* synchronizes cross-boundary IOs */
+ struct sx sc_disks_lock; /* synchronizes modification of sc_disks */
};
#define sc_name sc_geom->name
#endif /* _KERNEL */
Index: sys/geom/concat/g_concat.c
===================================================================
--- sys/geom/concat/g_concat.c
+++ sys/geom/concat/g_concat.c
@@ -35,6 +35,7 @@
#include <sys/module.h>
#include <sys/lock.h>
#include <sys/mutex.h>
+#include <sys/sx.h>
#include <sys/bio.h>
#include <sys/sbuf.h>
#include <sys/sysctl.h>
@@ -102,11 +103,14 @@
static u_int
g_concat_nvalid(struct g_concat_softc *sc)
{
- u_int i, no;
+ u_int no;
+ struct g_concat_disk *disk;
+ sx_assert(&sc->sc_disks_lock, SA_LOCKED);
+
no = 0;
- for (i = 0; i < sc->sc_ndisks; i++) {
- if (sc->sc_disks[i].d_consumer != NULL)
+ TAILQ_FOREACH(disk, &sc->sc_disks, d_next) {
+ if (disk->d_consumer != NULL)
no++;
}
@@ -172,10 +176,12 @@
struct g_consumer *cp1, *cp2, *tmp;
struct g_concat_disk *disk;
struct g_geom *gp;
+ struct g_concat_softc *sc;
int error;
g_topology_assert();
gp = pp->geom;
+ sc = gp->softc;
/* On first open, grab an extra "exclusive" bit */
if (pp->acr == 0 && pp->acw == 0 && pp->ace == 0)
@@ -184,6 +190,7 @@
if ((pp->acr + dr) == 0 && (pp->acw + dw) == 0 && (pp->ace + de) == 0)
de--;
+ sx_slock(&sc->sc_disks_lock);
LIST_FOREACH_SAFE(cp1, &gp->consumer, consumer, tmp) {
error = g_access(cp1, dr, dw, de);
if (error != 0)
@@ -194,9 +201,11 @@
g_concat_remove_disk(disk); /* May destroy geom. */
}
}
+ sx_sunlock(&sc->sc_disks_lock);
return (0);
fail:
+ sx_sunlock(&sc->sc_disks_lock);
LIST_FOREACH(cp2, &gp->consumer, consumer) {
if (cp1 == cp2)
break;
@@ -210,15 +219,15 @@
{
struct g_concat_softc *sc;
struct g_concat_disk *disk;
- int i, val;
+ int val;
sc = bp->bio_to->geom->softc;
- for (i = 0; i < sc->sc_ndisks; i++) {
- disk = &sc->sc_disks[i];
+ sx_assert(&sc->sc_disks_lock, SX_LOCKED);
+ TAILQ_FOREACH(disk, &sc->sc_disks, d_next) {
if (!disk->d_removed && disk->d_candelete)
break;
}
- val = i < sc->sc_ndisks;
+ val = disk != NULL;
g_handleattr(bp, "GEOM::candelete", &val, sizeof(val));
}
@@ -229,20 +238,19 @@
struct g_concat_disk *disk;
struct bio *cbp;
struct g_kerneldump *gkd;
- u_int i;
sc = bp->bio_to->geom->softc;
gkd = (struct g_kerneldump *)bp->bio_data;
- for (i = 0; i < sc->sc_ndisks; i++) {
- if (sc->sc_disks[i].d_start <= gkd->offset &&
- sc->sc_disks[i].d_end > gkd->offset)
+ TAILQ_FOREACH(disk, &sc->sc_disks, d_next) {
+ if (disk->d_start <= gkd->offset &&
+ disk->d_end > gkd->offset)
break;
}
- if (i == sc->sc_ndisks) {
+ if (disk == NULL) {
g_io_deliver(bp, EOPNOTSUPP);
return;
}
- disk = &sc->sc_disks[i];
+
gkd->offset -= disk->d_start;
if (gkd->length > disk->d_end - disk->d_start - gkd->offset)
gkd->length = disk->d_end - disk->d_start - gkd->offset;
@@ -265,16 +273,16 @@
pbp = bp->bio_parent;
sc = pbp->bio_to->geom->softc;
- mtx_lock(&sc->sc_lock);
+ mtx_lock(&sc->sc_completion_lock);
if (pbp->bio_error == 0)
pbp->bio_error = bp->bio_error;
pbp->bio_completed += bp->bio_completed;
pbp->bio_inbed++;
if (pbp->bio_children == pbp->bio_inbed) {
- mtx_unlock(&sc->sc_lock);
+ mtx_unlock(&sc->sc_completion_lock);
g_io_deliver(pbp, pbp->bio_error);
} else
- mtx_unlock(&sc->sc_lock);
+ mtx_unlock(&sc->sc_completion_lock);
g_destroy_bio(bp);
}
@@ -284,10 +292,12 @@
struct bio_queue_head queue;
struct g_consumer *cp;
struct bio *cbp;
- u_int no;
+ struct g_concat_disk *disk;
+ sx_assert(&sc->sc_disks_lock, SX_LOCKED);
+
bioq_init(&queue);
- for (no = 0; no < sc->sc_ndisks; no++) {
+ TAILQ_FOREACH(disk, &sc->sc_disks, d_next) {
cbp = g_clone_bio(bp);
if (cbp == NULL) {
while ((cbp = bioq_takefirst(&queue)) != NULL)
@@ -299,8 +309,8 @@
}
bioq_insert_tail(&queue, cbp);
cbp->bio_done = g_concat_done;
- cbp->bio_caller1 = sc->sc_disks[no].d_consumer;
- cbp->bio_to = sc->sc_disks[no].d_consumer->provider;
+ cbp->bio_caller1 = disk->d_consumer;
+ cbp->bio_to = disk->d_consumer->provider;
}
while ((cbp = bioq_takefirst(&queue)) != NULL) {
G_CONCAT_LOGREQ(cbp, "Sending request.");
@@ -320,7 +330,6 @@
off_t offset, end, length, off, len;
struct bio *cbp;
char *addr;
- u_int no;
pp = bp->bio_to;
sc = pp->geom->softc;
@@ -333,6 +342,7 @@
bp->bio_to->error, bp->bio_to->name));
G_CONCAT_LOGREQ(bp, "Request received.");
+ sx_slock(&sc->sc_disks_lock);
switch (bp->bio_cmd) {
case BIO_READ:
@@ -341,20 +351,20 @@
break;
case BIO_FLUSH:
g_concat_flush(sc, bp);
- return;
+ goto end;
case BIO_GETATTR:
if (strcmp("GEOM::kerneldump", bp->bio_attribute) == 0) {
g_concat_kernel_dump(bp);
- return;
+ goto end;
} else if (strcmp("GEOM::candelete", bp->bio_attribute) == 0) {
g_concat_candelete(bp);
- return;
+ goto end;
}
/* To which provider it should be delivered? */
/* FALLTHROUGH */
default:
g_io_deliver(bp, EOPNOTSUPP);
- return;
+ goto end;
}
offset = bp->bio_offset;
@@ -366,8 +376,7 @@
end = offset + length;
bioq_init(&queue);
- for (no = 0; no < sc->sc_ndisks; no++) {
- disk = &sc->sc_disks[no];
+ TAILQ_FOREACH(disk, &sc->sc_disks, d_next) {
if (disk->d_end <= offset)
continue;
if (disk->d_start >= end)
@@ -385,7 +394,7 @@
if (bp->bio_error == 0)
bp->bio_error = ENOMEM;
g_io_deliver(bp, bp->bio_error);
- return;
+ goto end;
}
bioq_insert_tail(&queue, cbp);
/*
@@ -421,6 +430,8 @@
cbp->bio_caller1 = NULL;
g_io_request(cbp, disk->d_consumer);
}
+end:
+ sx_sunlock(&sc->sc_disks_lock);
}
static void
@@ -428,7 +439,7 @@
{
struct g_concat_disk *disk;
struct g_provider *dp, *pp;
- u_int no, sectorsize = 0;
+ u_int sectorsize = 0;
off_t start;
int error;
@@ -440,8 +451,7 @@
pp->flags |= G_PF_DIRECT_SEND | G_PF_DIRECT_RECEIVE |
G_PF_ACCEPT_UNMAPPED;
start = 0;
- for (no = 0; no < sc->sc_ndisks; no++) {
- disk = &sc->sc_disks[no];
+ TAILQ_FOREACH(disk, &sc->sc_disks, d_next) {
dp = disk->d_consumer->provider;
disk->d_start = start;
disk->d_end = disk->d_start + dp->mediasize;
@@ -458,7 +468,7 @@
} else
G_CONCAT_DEBUG(1, "Failed to access disk %s, error %d.",
dp->name, error);
- if (no == 0)
+ if (disk == TAILQ_FIRST(&sc->sc_disks))
sectorsize = dp->sectorsize;
else
sectorsize = lcm(sectorsize, dp->sectorsize);
@@ -473,8 +483,9 @@
pp->sectorsize = sectorsize;
/* We have sc->sc_disks[sc->sc_ndisks - 1].d_end in 'start'. */
pp->mediasize = start;
- pp->stripesize = sc->sc_disks[0].d_consumer->provider->stripesize;
- pp->stripeoffset = sc->sc_disks[0].d_consumer->provider->stripeoffset;
+ dp = TAILQ_FIRST(&sc->sc_disks)->d_consumer->provider;
+ pp->stripesize = dp->stripesize;
+ pp->stripeoffset = dp->stripeoffset;
sc->sc_provider = pp;
g_error_provider(pp, 0);
@@ -521,14 +532,24 @@
int error;
g_topology_assert();
+
+ sx_slock(&sc->sc_disks_lock);
+
/* Metadata corrupted? */
- if (no >= sc->sc_ndisks)
+ if (no >= sc->sc_ndisks) {
+ sx_sunlock(&sc->sc_disks_lock);
return (EINVAL);
+ }
- disk = &sc->sc_disks[no];
+ for (disk = TAILQ_FIRST(&sc->sc_disks); no > 0; no--) {
+ disk = TAILQ_NEXT(disk, d_next);
+ }
+
/* Check if disk is not already attached. */
- if (disk->d_consumer != NULL)
+ if (disk->d_consumer != NULL) {
+ sx_sunlock(&sc->sc_disks_lock);
return (EEXIST);
+ }
gp = sc->sc_geom;
fcp = LIST_FIRST(&gp->consumer);
@@ -537,6 +558,7 @@
cp->flags |= G_CF_DIRECT_SEND | G_CF_DIRECT_RECEIVE;
error = g_attach(cp, pp);
if (error != 0) {
+ sx_sunlock(&sc->sc_disks_lock);
g_destroy_consumer(cp);
return (error);
}
@@ -544,6 +566,7 @@
if (fcp != NULL && (fcp->acr > 0 || fcp->acw > 0 || fcp->ace > 0)) {
error = g_access(cp, fcp->acr, fcp->acw, fcp->ace);
if (error != 0) {
+ sx_sunlock(&sc->sc_disks_lock);
g_detach(cp);
g_destroy_consumer(cp);
return (error);
@@ -552,8 +575,13 @@
if (sc->sc_type == G_CONCAT_TYPE_AUTOMATIC) {
struct g_concat_metadata md;
+ // temporarily give up the lock to avoid lock order violation
+ // due to topology unlock in g_concat_read_metadata
+ sx_sunlock(&sc->sc_disks_lock);
/* Re-read metadata. */
error = g_concat_read_metadata(cp, &md);
+ sx_slock(&sc->sc_disks_lock);
+
if (error != 0)
goto fail;
@@ -563,6 +591,10 @@
G_CONCAT_DEBUG(0, "Metadata on %s changed.", pp->name);
goto fail;
}
+
+ disk->d_hardcoded = md.md_provider[0] != '\0';
+ } else {
+ disk->d_hardcoded = false;
}
cp->private = disk;
@@ -575,9 +607,11 @@
G_CONCAT_DEBUG(0, "Disk %s attached to %s.", pp->name, sc->sc_name);
g_concat_check_and_run(sc);
+ sx_sunlock(&sc->sc_disks_lock); // need lock for check_and_run
return (0);
fail:
+ sx_sunlock(&sc->sc_disks_lock);
if (fcp != NULL && (fcp->acr > 0 || fcp->acw > 0 || fcp->ace > 0))
g_access(cp, -fcp->acr, -fcp->acw, -fcp->ace);
g_detach(cp);
@@ -590,6 +624,7 @@
u_int type)
{
struct g_concat_softc *sc;
+ struct g_concat_disk *disk;
struct g_geom *gp;
u_int no;
@@ -619,12 +654,14 @@
sc->sc_id = md->md_id;
sc->sc_ndisks = md->md_all;
- sc->sc_disks = malloc(sizeof(struct g_concat_disk) * sc->sc_ndisks,
- M_CONCAT, M_WAITOK | M_ZERO);
- for (no = 0; no < sc->sc_ndisks; no++)
- sc->sc_disks[no].d_consumer = NULL;
+ TAILQ_INIT(&sc->sc_disks);
+ for (no = 0; no < sc->sc_ndisks; no++) {
+ disk = malloc(sizeof(*disk), M_CONCAT, M_WAITOK | M_ZERO);
+ TAILQ_INSERT_TAIL(&sc->sc_disks, disk, d_next);
+ }
sc->sc_type = type;
- mtx_init(&sc->sc_lock, "gconcat lock", NULL, MTX_DEF);
+ mtx_init(&sc->sc_completion_lock, "gconcat lock", NULL, MTX_DEF);
+ sx_init(&sc->sc_disks_lock, "gconcat append lock");
gp->softc = sc;
sc->sc_geom = gp;
@@ -641,6 +678,7 @@
struct g_provider *pp;
struct g_consumer *cp, *cp1;
struct g_geom *gp;
+ struct g_concat_disk *disk;
g_topology_assert();
@@ -672,8 +710,12 @@
gp->softc = NULL;
KASSERT(sc->sc_provider == NULL, ("Provider still exists? (device=%s)",
gp->name));
- free(sc->sc_disks, M_CONCAT);
- mtx_destroy(&sc->sc_lock);
+ while ((disk = TAILQ_FIRST(&sc->sc_disks)) != NULL) {
+ TAILQ_REMOVE(&sc->sc_disks, disk, d_next);
+ free(disk, M_CONCAT);
+ }
+ mtx_destroy(&sc->sc_completion_lock);
+ sx_destroy(&sc->sc_disks_lock);
free(sc, M_CONCAT);
G_CONCAT_DEBUG(0, "Device %s destroyed.", gp->name);
@@ -949,7 +991,204 @@
}
}
+static struct g_concat_disk *
+g_concat_find_disk(struct g_concat_softc *sc, const char *name)
+{
+ struct g_concat_disk *disk;
+
+ sx_assert(&sc->sc_disks_lock, SX_LOCKED);
+ if (strncmp(name, "/dev/", 5) == 0)
+ name += 5;
+ TAILQ_FOREACH(disk, &sc->sc_disks, d_next) {
+ if (disk->d_consumer == NULL)
+ continue;
+ if (disk->d_consumer->provider == NULL)
+ continue;
+ if (strcmp(disk->d_consumer->provider->name, name) == 0)
+ return (disk);
+ }
+ return (NULL);
+}
+
static void
+g_concat_write_metadata(struct gctl_req *req, struct g_concat_softc *sc)
+{
+ u_int no = 0;
+ struct g_concat_disk *disk;
+ struct g_concat_metadata md;
+ struct g_provider *pp;
+ u_char *sector;
+ int error;
+
+ strlcpy(md.md_magic, G_CONCAT_MAGIC, sizeof(md.md_magic));
+ md.md_version = G_CONCAT_VERSION;
+ strlcpy(md.md_name, sc->sc_name, sizeof(md.md_name));
+ md.md_id = sc->sc_id;
+ md.md_all = sc->sc_ndisks;
+ TAILQ_FOREACH(disk, &sc->sc_disks, d_next) {
+ pp = disk->d_consumer->provider;
+
+ md.md_no = no;
+ bzero(md.md_provider, sizeof(md.md_provider));
+ if (disk->d_hardcoded) {
+ strlcpy(md.md_provider, pp->name, sizeof(md.md_provider));
+ }
+ md.md_provsize = disk->d_consumer->provider->mediasize;
+
+ sector = g_malloc(pp->sectorsize, M_WAITOK);
+
+ concat_metadata_encode(&md, sector);
+ error = g_access(disk->d_consumer, 0, 1, 0);
+ if (error == 0) {
+ error = g_write_data(disk->d_consumer, pp->mediasize - pp->sectorsize,
+ sector, pp->sectorsize);
+ (void)g_access(disk->d_consumer, 0, -1, 0);
+ }
+ g_free(sector);
+ if (error != 0) {
+ gctl_error(req, "Cannot store metadata on %s: %d", pp->name, error);
+ }
+
+ no++;
+ }
+}
+
+static void
+g_concat_ctl_append(struct gctl_req *req, struct g_class *mp)
+{
+ struct g_concat_softc *sc;
+ struct g_consumer *cp, *fcp;
+ struct g_provider *pp;
+ struct g_geom *gp;
+ const char *name, *cname;
+ struct g_concat_disk *disk;
+ int *nargs, *hardcode;
+ int error;
+ int disk_candelete;
+
+ g_topology_assert();
+
+ nargs = gctl_get_paraml(req, "nargs", sizeof(*nargs));
+ if (nargs == NULL) {
+ gctl_error(req, "No '%s' argument.", "nargs");
+ return;
+ }
+ if (*nargs != 2) {
+ gctl_error(req, "Invalid number of arguments.");
+ return;
+ }
+ hardcode = gctl_get_paraml(req, "hardcode", sizeof(*hardcode));
+ if (hardcode == NULL) {
+ gctl_error(req, "No '%s' argument.", "hardcode");
+ return;
+ }
+
+ cname = gctl_get_asciiparam(req, "arg0");
+ if (cname == NULL) {
+ gctl_error(req, "No 'arg%u' argument.", 0);
+ return;
+ }
+ sc = g_concat_find_device(mp, cname);
+ if (sc == NULL) {
+ gctl_error(req, "No such device: %s.", cname);
+ return;
+ }
+ if (sc->sc_provider == NULL) {
+ /*
+ * this won't race with g_concat_remove_disk as both
+ * are holding the topology lock
+ */
+ gctl_error(req, "Device not active, can't append: %s.", cname);
+ return;
+ }
+ G_CONCAT_DEBUG(1, "Appending to %s:", cname);
+ sx_xlock(&sc->sc_disks_lock);
+ gp = sc->sc_geom;
+ fcp = LIST_FIRST(&gp->consumer);
+
+ name = gctl_get_asciiparam(req, "arg1");
+ if (name == NULL) {
+ gctl_error(req, "No 'arg%u' argument.", 1);
+ goto fail;
+ }
+ if (strncmp(name, "/dev/", strlen("/dev/")) == 0)
+ name += strlen("/dev/");
+ pp = g_provider_by_name(name);
+ if (pp == NULL) {
+ G_CONCAT_DEBUG(1, "Disk %s is invalid.", name);
+ gctl_error(req, "Disk %s is invalid.", name);
+ goto fail;
+ }
+ G_CONCAT_DEBUG(1, "Appending %s to this", name);
+
+ if (g_concat_find_disk(sc, name) != NULL) {
+ gctl_error(req, "Disk %s already appended.", name);
+ goto fail;
+ }
+
+ if ((sc->sc_provider->sectorsize % pp->sectorsize) != 0) {
+ gctl_error(req, "Providers sectorsize mismatch: %u vs %u",
+ sc->sc_provider->sectorsize, pp->sectorsize);
+ goto fail;
+ }
+
+ cp = g_new_consumer(gp);
+ cp->flags |= G_CF_DIRECT_SEND | G_CF_DIRECT_RECEIVE;
+ error = g_attach(cp, pp);
+ if (error != 0) {
+ g_destroy_consumer(cp);
+ gctl_error(req, "Cannot open device %s (error=%d).",
+ name, error);
+ goto fail;
+ }
+
+ error = g_access(cp, 1, 0, 0);
+ if (error == 0) {
+ error = g_getattr("GEOM::candelete", cp, &disk_candelete);
+ if (error != 0)
+ disk_candelete = 0;
+ (void)g_access(cp, -1, 0, 0);
+ } else
+ G_CONCAT_DEBUG(1, "Failed to access disk %s, error %d.", name, error);
+
+ /* invoke g_access exactly as deep as all the other members currently are */
+ if (fcp != NULL && (fcp->acr > 0 || fcp->acw > 0 || fcp->ace > 0)) {
+ error = g_access(cp, fcp->acr, fcp->acw, fcp->ace);
+ if (error != 0) {
+ g_detach(cp);
+ g_destroy_consumer(cp);
+ gctl_error(req, "Failed to access disk %s (error=%d).", name, error);
+ goto fail;
+ }
+ }
+
+ disk = malloc(sizeof(*disk), M_CONCAT, M_WAITOK | M_ZERO);
+ disk->d_consumer = cp;
+ disk->d_softc = sc;
+ disk->d_start = TAILQ_LAST(&sc->sc_disks, g_concat_disks)->d_end;
+ disk->d_end = disk->d_start + cp->provider->mediasize;
+ disk->d_candelete = disk_candelete;
+ disk->d_removed = 0;
+ disk->d_hardcoded = *hardcode;
+ cp->private = disk;
+ TAILQ_INSERT_TAIL(&sc->sc_disks, disk, d_next);
+ sc->sc_ndisks++;
+
+ if (sc->sc_type == G_CONCAT_TYPE_AUTOMATIC) {
+ /* last sector is for metadata */
+ disk->d_end -= cp->provider->sectorsize;
+
+ /* update metadata on all parts */
+ g_concat_write_metadata(req, sc);
+ }
+
+ g_resize_provider(sc->sc_provider, disk->d_end);
+
+fail:
+ sx_xunlock(&sc->sc_disks_lock);
+}
+
+static void
g_concat_config(struct gctl_req *req, struct g_class *mp, const char *verb)
{
uint32_t *version;
@@ -973,6 +1212,9 @@
strcmp(verb, "stop") == 0) {
g_concat_ctl_destroy(req, mp);
return;
+ } else if (strcmp(verb, "append") == 0) {
+ g_concat_ctl_append(req, mp);
+ return;
}
gctl_error(req, "Unknown verb.");
}
@@ -987,6 +1229,8 @@
sc = gp->softc;
if (sc == NULL)
return;
+
+ sx_slock(&sc->sc_disks_lock);
if (pp != NULL) {
/* Nothing here. */
} else if (cp != NULL) {
@@ -994,7 +1238,7 @@
disk = cp->private;
if (disk == NULL)
- return;
+ goto end;
sbuf_printf(sb, "%s<End>%jd</End>\n", indent,
(intmax_t)disk->d_end);
sbuf_printf(sb, "%s<Start>%jd</Start>\n", indent,
@@ -1023,6 +1267,8 @@
sbuf_printf(sb, "DOWN");
sbuf_printf(sb, "</State>\n");
}
+end:
+ sx_sunlock(&sc->sc_disks_lock);
}
DECLARE_GEOM_CLASS(g_concat_class, g_concat);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Jun 18, 2:01 PM (16 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
34052057
Default Alt Text
D19227.diff (17 KB)
Attached To
Mode
D19227: geom: add feature: gconcat online append
Attached
Detach File
Event Timeline
Log In to Comment