Skip to content

Commit 2efc924

Browse files
committed
Detoast plpgsql variables if they might live across a transaction boundary.
Up to now, it's been safe for plpgsql to store TOAST pointers in its variables because the ActiveSnapshot for whatever query called the plpgsql function will surely protect such TOAST values from being vacuumed away, even if the owning table rows are committed dead. With the introduction of procedures, that assumption is no longer good in "non atomic" executions of plpgsql code. We adopt the slightly brute-force solution of detoasting all TOAST pointers at the time they are stored into variables, if we're in a non-atomic context, just in case the owning row goes away. Some care is needed to avoid long-term memory leaks, since plpgsql tends to run with CurrentMemoryContext pointing to its call-lifespan context, but we shouldn't assume that no memory is leaked by heap_tuple_fetch_attr. In plpgsql proper, we can do the detoasting work in the "eval_mcontext". Most of the code thrashing here is due to the need to add this capability to expandedrecord.c as well as plpgsql proper. In expandedrecord.c, we can't assume that the caller's context is short-lived, so make use of the short-term sub-context that was already invented for checking domain constraints. In view of this repurposing, it seems good to rename that variable and associated code from "domain_check_cxt" to "short_term_cxt". Peter Eisentraut and Tom Lane Discussion: https://postgr.es/m/[email protected]
1 parent a11b3bd commit 2efc924

File tree

7 files changed

+522
-75
lines changed

7 files changed

+522
-75
lines changed

src/backend/utils/adt/expandedrecord.c

+124-56
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "postgres.h"
2020

2121
#include "access/htup_details.h"
22+
#include "access/tuptoaster.h"
2223
#include "catalog/heap.h"
2324
#include "catalog/pg_type.h"
2425
#include "utils/builtins.h"
@@ -41,7 +42,7 @@ static const ExpandedObjectMethods ER_methods =
4142

4243
/* Other local functions */
4344
static void ER_mc_callback(void *arg);
44-
static MemoryContext get_domain_check_cxt(ExpandedRecordHeader *erh);
45+
static MemoryContext get_short_term_cxt(ExpandedRecordHeader *erh);
4546
static void build_dummy_expanded_header(ExpandedRecordHeader *main_erh);
4647
static pg_noinline void check_domain_for_new_field(ExpandedRecordHeader *erh,
4748
int fnumber,
@@ -57,8 +58,9 @@ static pg_noinline void check_domain_for_new_tuple(ExpandedRecordHeader *erh,
5758
*
5859
* The expanded record is initially "empty", having a state logically
5960
* equivalent to a NULL composite value (not ROW(NULL, NULL, ...)).
60-
* Note that this might not be a valid state for a domain type; if the
61-
* caller needs to check that, call expanded_record_set_tuple(erh, NULL).
61+
* Note that this might not be a valid state for a domain type;
62+
* if the caller needs to check that, call
63+
* expanded_record_set_tuple(erh, NULL, false, false).
6264
*
6365
* The expanded object will be a child of parentcontext.
6466
*/
@@ -424,16 +426,20 @@ make_expanded_record_from_exprecord(ExpandedRecordHeader *olderh,
424426
*
425427
* The tuple is physically copied into the expanded record's local storage
426428
* if "copy" is true, otherwise it's caller's responsibility that the tuple
427-
* will live as long as the expanded record does. In any case, out-of-line
428-
* fields in the tuple are not automatically inlined.
429+
* will live as long as the expanded record does.
430+
*
431+
* Out-of-line field values in the tuple are automatically inlined if
432+
* "expand_external" is true, otherwise not. (The combination copy = false,
433+
* expand_external = true is not sensible and not supported.)
429434
*
430435
* Alternatively, tuple can be NULL, in which case we just set the expanded
431436
* record to be empty.
432437
*/
433438
void
434439
expanded_record_set_tuple(ExpandedRecordHeader *erh,
435440
HeapTuple tuple,
436-
bool copy)
441+
bool copy,
442+
bool expand_external)
437443
{
438444
int oldflags;
439445
HeapTuple oldtuple;
@@ -452,6 +458,25 @@ expanded_record_set_tuple(ExpandedRecordHeader *erh,
452458
if (erh->flags & ER_FLAG_IS_DOMAIN)
453459
check_domain_for_new_tuple(erh, tuple);
454460

461+
/*
462+
* If we need to get rid of out-of-line field values, do so, using the
463+
* short-term context to avoid leaking whatever cruft the toast fetch
464+
* might generate.
465+
*/
466+
if (expand_external && tuple)
467+
{
468+
/* Assert caller didn't ask for unsupported case */
469+
Assert(copy);
470+
if (HeapTupleHasExternal(tuple))
471+
{
472+
oldcxt = MemoryContextSwitchTo(get_short_term_cxt(erh));
473+
tuple = toast_flatten_tuple(tuple, erh->er_tupdesc);
474+
MemoryContextSwitchTo(oldcxt);
475+
}
476+
else
477+
expand_external = false; /* need not clean up below */
478+
}
479+
455480
/*
456481
* Initialize new flags, keeping only non-data status bits.
457482
*/
@@ -468,6 +493,10 @@ expanded_record_set_tuple(ExpandedRecordHeader *erh,
468493
newtuple = heap_copytuple(tuple);
469494
newflags |= ER_FLAG_FVALUE_ALLOCED;
470495
MemoryContextSwitchTo(oldcxt);
496+
497+
/* We can now flush anything that detoasting might have leaked. */
498+
if (expand_external)
499+
MemoryContextReset(erh->er_short_term_cxt);
471500
}
472501
else
473502
newtuple = tuple;
@@ -676,23 +705,13 @@ ER_get_flat_size(ExpandedObjectHeader *eohptr)
676705
VARATT_IS_EXTERNAL(DatumGetPointer(erh->dvalues[i])))
677706
{
678707
/*
679-
* It's an external toasted value, so we need to dereference
680-
* it so that the flat representation will be self-contained.
681-
* Do this step in the caller's context because the TOAST
682-
* fetch might leak memory. That means making an extra copy,
683-
* which is a tad annoying, but repetitive leaks in the
684-
* record's context would be worse.
708+
* expanded_record_set_field_internal can do the actual work
709+
* of detoasting. It needn't recheck domain constraints.
685710
*/
686-
Datum newValue;
687-
688-
newValue = PointerGetDatum(PG_DETOAST_DATUM(erh->dvalues[i]));
689-
/* expanded_record_set_field can do the rest */
690-
/* ... and we don't need it to recheck domain constraints */
691711
expanded_record_set_field_internal(erh, i + 1,
692-
newValue, false,
712+
erh->dvalues[i], false,
713+
true,
693714
false);
694-
/* Might as well free the detoasted value */
695-
pfree(DatumGetPointer(newValue));
696715
}
697716
}
698717

@@ -1087,12 +1106,16 @@ expanded_record_fetch_field(ExpandedRecordHeader *erh, int fnumber,
10871106
* (without changing the record's state) if the domain's constraints would
10881107
* be violated.
10891108
*
1109+
* If expand_external is true and newValue is an out-of-line value, we'll
1110+
* forcibly detoast it so that the record does not depend on external storage.
1111+
*
10901112
* Internal callers can pass check_constraints = false to skip application
10911113
* of domain constraints. External callers should never do that.
10921114
*/
10931115
void
10941116
expanded_record_set_field_internal(ExpandedRecordHeader *erh, int fnumber,
10951117
Datum newValue, bool isnull,
1118+
bool expand_external,
10961119
bool check_constraints)
10971120
{
10981121
TupleDesc tupdesc;
@@ -1124,23 +1147,46 @@ expanded_record_set_field_internal(ExpandedRecordHeader *erh, int fnumber,
11241147
elog(ERROR, "cannot assign to field %d of expanded record", fnumber);
11251148

11261149
/*
1127-
* Copy new field value into record's context, if needed.
1150+
* Copy new field value into record's context, and deal with detoasting,
1151+
* if needed.
11281152
*/
11291153
attr = TupleDescAttr(tupdesc, fnumber - 1);
11301154
if (!isnull && !attr->attbyval)
11311155
{
11321156
MemoryContext oldcxt;
11331157

1158+
/* If requested, detoast any external value */
1159+
if (expand_external)
1160+
{
1161+
if (attr->attlen == -1 &&
1162+
VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
1163+
{
1164+
/* Detoasting should be done in short-lived context. */
1165+
oldcxt = MemoryContextSwitchTo(get_short_term_cxt(erh));
1166+
newValue = PointerGetDatum(heap_tuple_fetch_attr((struct varlena *) DatumGetPointer(newValue)));
1167+
MemoryContextSwitchTo(oldcxt);
1168+
}
1169+
else
1170+
expand_external = false; /* need not clean up below */
1171+
}
1172+
1173+
/* Copy value into record's context */
11341174
oldcxt = MemoryContextSwitchTo(erh->hdr.eoh_context);
11351175
newValue = datumCopy(newValue, false, attr->attlen);
11361176
MemoryContextSwitchTo(oldcxt);
11371177

1178+
/* We can now flush anything that detoasting might have leaked */
1179+
if (expand_external)
1180+
MemoryContextReset(erh->er_short_term_cxt);
1181+
11381182
/* Remember that we have field(s) that may need to be pfree'd */
11391183
erh->flags |= ER_FLAG_DVALUES_ALLOCED;
11401184

11411185
/*
11421186
* While we're here, note whether it's an external toasted value,
1143-
* because that could mean we need to inline it later.
1187+
* because that could mean we need to inline it later. (Think not to
1188+
* merge this into the previous expand_external logic: datumCopy could
1189+
* by itself have made the value non-external.)
11441190
*/
11451191
if (attr->attlen == -1 &&
11461192
VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
@@ -1193,14 +1239,20 @@ expanded_record_set_field_internal(ExpandedRecordHeader *erh, int fnumber,
11931239
* Caller must ensure that the provided datums are of the right types
11941240
* to match the record's previously assigned rowtype.
11951241
*
1242+
* If expand_external is true, we'll forcibly detoast out-of-line field values
1243+
* so that the record does not depend on external storage.
1244+
*
11961245
* Unlike repeated application of expanded_record_set_field(), this does not
11971246
* guarantee to leave the expanded record in a non-corrupt state in event
11981247
* of an error. Typically it would only be used for initializing a new
1199-
* expanded record.
1248+
* expanded record. Also, because we expect this to be applied at most once
1249+
* in the lifespan of an expanded record, we do not worry about any cruft
1250+
* that detoasting might leak.
12001251
*/
12011252
void
12021253
expanded_record_set_fields(ExpandedRecordHeader *erh,
1203-
const Datum *newValues, const bool *isnulls)
1254+
const Datum *newValues, const bool *isnulls,
1255+
bool expand_external)
12041256
{
12051257
TupleDesc tupdesc;
12061258
Datum *dvalues;
@@ -1245,22 +1297,37 @@ expanded_record_set_fields(ExpandedRecordHeader *erh,
12451297
if (!attr->attbyval)
12461298
{
12471299
/*
1248-
* Copy new field value into record's context, if needed.
1300+
* Copy new field value into record's context, and deal with
1301+
* detoasting, if needed.
12491302
*/
12501303
if (!isnull)
12511304
{
1252-
newValue = datumCopy(newValue, false, attr->attlen);
1305+
/* Is it an external toasted value? */
1306+
if (attr->attlen == -1 &&
1307+
VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
1308+
{
1309+
if (expand_external)
1310+
{
1311+
/* Detoast as requested while copying the value */
1312+
newValue = PointerGetDatum(heap_tuple_fetch_attr((struct varlena *) DatumGetPointer(newValue)));
1313+
}
1314+
else
1315+
{
1316+
/* Just copy the value */
1317+
newValue = datumCopy(newValue, false, -1);
1318+
/* If it's still external, remember that */
1319+
if (VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
1320+
erh->flags |= ER_FLAG_HAVE_EXTERNAL;
1321+
}
1322+
}
1323+
else
1324+
{
1325+
/* Not an external value, just copy it */
1326+
newValue = datumCopy(newValue, false, attr->attlen);
1327+
}
12531328

12541329
/* Remember that we have field(s) that need to be pfree'd */
12551330
erh->flags |= ER_FLAG_DVALUES_ALLOCED;
1256-
1257-
/*
1258-
* While we're here, note whether it's an external toasted
1259-
* value, because that could mean we need to inline it later.
1260-
*/
1261-
if (attr->attlen == -1 &&
1262-
VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
1263-
erh->flags |= ER_FLAG_HAVE_EXTERNAL;
12641331
}
12651332

12661333
/*
@@ -1291,7 +1358,7 @@ expanded_record_set_fields(ExpandedRecordHeader *erh,
12911358
if (erh->flags & ER_FLAG_IS_DOMAIN)
12921359
{
12931360
/* We run domain_check in a short-lived context to limit cruft */
1294-
MemoryContextSwitchTo(get_domain_check_cxt(erh));
1361+
MemoryContextSwitchTo(get_short_term_cxt(erh));
12951362

12961363
domain_check(ExpandedRecordGetRODatum(erh), false,
12971364
erh->er_decltypeid,
@@ -1303,25 +1370,26 @@ expanded_record_set_fields(ExpandedRecordHeader *erh,
13031370
}
13041371

13051372
/*
1306-
* Construct (or reset) working memory context for domain checks.
1373+
* Construct (or reset) working memory context for short-term operations.
1374+
*
1375+
* This context is used for domain check evaluation and for detoasting.
13071376
*
1308-
* If we don't have a working memory context for domain checking, make one;
1309-
* if we have one, reset it to get rid of any leftover cruft. (It is a tad
1310-
* annoying to need a whole context for this, since it will often go unused
1311-
* --- but it's hard to avoid memory leaks otherwise. We can make the
1312-
* context small, at least.)
1377+
* If we don't have a short-lived memory context, make one; if we have one,
1378+
* reset it to get rid of any leftover cruft. (It is a tad annoying to need a
1379+
* whole context for this, since it will often go unused --- but it's hard to
1380+
* avoid memory leaks otherwise. We can make the context small, at least.)
13131381
*/
13141382
static MemoryContext
1315-
get_domain_check_cxt(ExpandedRecordHeader *erh)
1383+
get_short_term_cxt(ExpandedRecordHeader *erh)
13161384
{
1317-
if (erh->er_domain_check_cxt == NULL)
1318-
erh->er_domain_check_cxt =
1385+
if (erh->er_short_term_cxt == NULL)
1386+
erh->er_short_term_cxt =
13191387
AllocSetContextCreate(erh->hdr.eoh_context,
1320-
"expanded record domain checks",
1388+
"expanded record short-term context",
13211389
ALLOCSET_SMALL_SIZES);
13221390
else
1323-
MemoryContextReset(erh->er_domain_check_cxt);
1324-
return erh->er_domain_check_cxt;
1391+
MemoryContextReset(erh->er_short_term_cxt);
1392+
return erh->er_short_term_cxt;
13251393
}
13261394

13271395
/*
@@ -1340,8 +1408,8 @@ build_dummy_expanded_header(ExpandedRecordHeader *main_erh)
13401408
ExpandedRecordHeader *erh;
13411409
TupleDesc tupdesc = expanded_record_get_tupdesc(main_erh);
13421410

1343-
/* Ensure we have a domain_check_cxt */
1344-
(void) get_domain_check_cxt(main_erh);
1411+
/* Ensure we have a short-lived context */
1412+
(void) get_short_term_cxt(main_erh);
13451413

13461414
/*
13471415
* Allocate dummy header on first time through, or in the unlikely event
@@ -1372,7 +1440,7 @@ build_dummy_expanded_header(ExpandedRecordHeader *main_erh)
13721440
* nothing else is authorized to delete or transfer ownership of the
13731441
* object's context, so it should be safe enough.
13741442
*/
1375-
EOH_init_header(&erh->hdr, &ER_methods, main_erh->er_domain_check_cxt);
1443+
EOH_init_header(&erh->hdr, &ER_methods, main_erh->er_short_term_cxt);
13761444
erh->er_magic = ER_MAGIC;
13771445

13781446
/* Set up dvalues/dnulls, with no valid contents as yet */
@@ -1488,7 +1556,7 @@ check_domain_for_new_field(ExpandedRecordHeader *erh, int fnumber,
14881556
* We call domain_check in the short-lived context, so that any cruft
14891557
* leaked by expression evaluation can be reclaimed.
14901558
*/
1491-
oldcxt = MemoryContextSwitchTo(erh->er_domain_check_cxt);
1559+
oldcxt = MemoryContextSwitchTo(erh->er_short_term_cxt);
14921560

14931561
/*
14941562
* And now we can apply the check. Note we use main header's domain cache
@@ -1502,7 +1570,7 @@ check_domain_for_new_field(ExpandedRecordHeader *erh, int fnumber,
15021570
MemoryContextSwitchTo(oldcxt);
15031571

15041572
/* We might as well clean up cruft immediately. */
1505-
MemoryContextReset(erh->er_domain_check_cxt);
1573+
MemoryContextReset(erh->er_short_term_cxt);
15061574
}
15071575

15081576
/*
@@ -1518,7 +1586,7 @@ check_domain_for_new_tuple(ExpandedRecordHeader *erh, HeapTuple tuple)
15181586
if (tuple == NULL)
15191587
{
15201588
/* We run domain_check in a short-lived context to limit cruft */
1521-
oldcxt = MemoryContextSwitchTo(get_domain_check_cxt(erh));
1589+
oldcxt = MemoryContextSwitchTo(get_short_term_cxt(erh));
15221590

15231591
domain_check((Datum) 0, true,
15241592
erh->er_decltypeid,
@@ -1528,7 +1596,7 @@ check_domain_for_new_tuple(ExpandedRecordHeader *erh, HeapTuple tuple)
15281596
MemoryContextSwitchTo(oldcxt);
15291597

15301598
/* We might as well clean up cruft immediately. */
1531-
MemoryContextReset(erh->er_domain_check_cxt);
1599+
MemoryContextReset(erh->er_short_term_cxt);
15321600

15331601
return;
15341602
}
@@ -1551,7 +1619,7 @@ check_domain_for_new_tuple(ExpandedRecordHeader *erh, HeapTuple tuple)
15511619
* We call domain_check in the short-lived context, so that any cruft
15521620
* leaked by expression evaluation can be reclaimed.
15531621
*/
1554-
oldcxt = MemoryContextSwitchTo(erh->er_domain_check_cxt);
1622+
oldcxt = MemoryContextSwitchTo(erh->er_short_term_cxt);
15551623

15561624
/*
15571625
* And now we can apply the check. Note we use main header's domain cache
@@ -1565,5 +1633,5 @@ check_domain_for_new_tuple(ExpandedRecordHeader *erh, HeapTuple tuple)
15651633
MemoryContextSwitchTo(oldcxt);
15661634

15671635
/* We might as well clean up cruft immediately. */
1568-
MemoryContextReset(erh->er_domain_check_cxt);
1636+
MemoryContextReset(erh->er_short_term_cxt);
15691637
}

src/include/postgres.h

+2
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,8 @@ typedef struct
321321
(VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_EXPANDED_RW)
322322
#define VARATT_IS_EXTERNAL_EXPANDED(PTR) \
323323
(VARATT_IS_EXTERNAL(PTR) && VARTAG_IS_EXPANDED(VARTAG_EXTERNAL(PTR)))
324+
#define VARATT_IS_EXTERNAL_NON_EXPANDED(PTR) \
325+
(VARATT_IS_EXTERNAL(PTR) && !VARTAG_IS_EXPANDED(VARTAG_EXTERNAL(PTR)))
324326
#define VARATT_IS_SHORT(PTR) VARATT_IS_1B(PTR)
325327
#define VARATT_IS_EXTENDED(PTR) (!VARATT_IS_4B_U(PTR))
326328

0 commit comments

Comments
 (0)