Fix bogus "out of memory" reports in tuplestore.c.
authorTom Lane <tgl@sss.pgh.pa.us>
Tue, 4 Aug 2015 22:18:46 +0000 (18:18 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Tue, 4 Aug 2015 22:18:46 +0000 (18:18 -0400)
The tuplesort/tuplestore memory management logic assumed that the chunk
allocation overhead for its memtuples array could not increase when
increasing the array size.  This is and always was true for tuplesort,
but we (I, I think) blindly copied that logic into tuplestore.c without
noticing that the assumption failed to hold for the much smaller array
elements used by tuplestore.  Given rather small work_mem, this could
result in an improper complaint about "unexpected out-of-memory situation",
as reported by Brent DeSpain in bug #13530.

The easiest way to fix this is just to increase tuplestore's initial
array size so that the assumption holds.  Rather than relying on magic
constants, though, let's export a #define from aset.c that represents
the safe allocation threshold, and make tuplestore's calculation depend
on that.

Do the same in tuplesort.c to keep the logic looking parallel, even though
tuplesort.c isn't actually at risk at present.  This will keep us from
breaking it if we ever muck with the allocation parameters in aset.c.

Back-patch to all supported versions.  The error message doesn't occur
pre-9.3, not so much because the problem can't happen as because the
pre-9.3 tuplestore code neglected to check for it.  (The chance of
trouble is a great deal larger as of 9.3, though, due to changes in the
array-size-increasing strategy.)  However, allowing LACKMEM() to become
true unexpectedly could still result in less-than-desirable behavior,
so let's patch it all the way back.

src/backend/utils/mmgr/aset.c
src/backend/utils/sort/tuplesort.c
src/backend/utils/sort/tuplestore.c
src/include/utils/memutils.h

index 0cfb934b003262c8eff828f424ffadaa7b5242c0..febeb6eaf8eb966f7c50e4368b4af5c67ec205d2 100644 (file)
  *
  * With the current parameters, request sizes up to 8K are treated as chunks,
  * larger requests go into dedicated blocks.  Change ALLOCSET_NUM_FREELISTS
- * to adjust the boundary point.  (But in contexts with small maxBlockSize,
- * we may set the allocChunkLimit to less than 8K, so as to avoid space
- * wastage.)
+ * to adjust the boundary point; and adjust ALLOCSET_SEPARATE_THRESHOLD in
+ * memutils.h to agree.  (Note: in contexts with small maxBlockSize, we may
+ * set the allocChunkLimit to less than 8K, so as to avoid space wastage.)
  *--------------------
  */
 
@@ -476,7 +476,12 @@ AllocSetContextCreate(MemoryContext parent,
         * We have to have allocChunkLimit a power of two, because the requested
         * and actually-allocated sizes of any chunk must be on the same side of
         * the limit, else we get confused about whether the chunk is "big".
+        *
+        * Also, allocChunkLimit must not exceed ALLOCSET_SEPARATE_THRESHOLD.
         */
+       StaticAssertStmt(ALLOC_CHUNK_LIMIT == ALLOCSET_SEPARATE_THRESHOLD,
+                                        "ALLOC_CHUNK_LIMIT != ALLOCSET_SEPARATE_THRESHOLD");
+
        set->allocChunkLimit = ALLOC_CHUNK_LIMIT;
        while ((Size) (set->allocChunkLimit + ALLOC_CHUNKHDRSZ) >
                   (Size) ((maxBlockSize - ALLOC_BLOCKHDRSZ) / ALLOC_CHUNK_FRACTION))
index 1e62a73eed324f2cd885af000c0035375aab5254..d532e87450e64fbdf5055b443926771be01cd9fe 100644 (file)
@@ -577,7 +577,14 @@ tuplesort_begin_common(int workMem, bool randomAccess)
        state->tapeset = NULL;
 
        state->memtupcount = 0;
-       state->memtupsize = 1024;       /* initial guess */
+
+       /*
+        * Initial size of array must be more than ALLOCSET_SEPARATE_THRESHOLD;
+        * see comments in grow_memtuples().
+        */
+       state->memtupsize = Max(1024,
+                                               ALLOCSET_SEPARATE_THRESHOLD / sizeof(SortTuple) + 1);
+
        state->growmemtuples = true;
        state->memtuples = (SortTuple *) palloc(state->memtupsize * sizeof(SortTuple));
 
@@ -1165,10 +1172,10 @@ grow_memtuples(Tuplesortstate *state)
         * never generate a dangerous request, but to be safe, check explicitly
         * that the array growth fits within availMem.  (We could still cause
         * LACKMEM if the memory chunk overhead associated with the memtuples
-        * array were to increase.  That shouldn't happen with any sane value of
-        * allowedMem, because at any array size large enough to risk LACKMEM,
-        * palloc would be treating both old and new arrays as separate chunks.
-        * But we'll check LACKMEM explicitly below just in case.)
+        * array were to increase.  That shouldn't happen because we chose the
+        * initial array size large enough to ensure that palloc will be treating
+        * both old and new arrays as separate chunks.  But we'll check LACKMEM
+        * explicitly below just in case.)
         */
        if (state->availMem < (int64) ((newmemtupsize - memtupsize) * sizeof(SortTuple)))
                goto noalloc;
@@ -1181,7 +1188,7 @@ grow_memtuples(Tuplesortstate *state)
                                          state->memtupsize * sizeof(SortTuple));
        USEMEM(state, GetMemoryChunkSpace(state->memtuples));
        if (LACKMEM(state))
-               elog(ERROR, "unexpected out-of-memory situation during sort");
+               elog(ERROR, "unexpected out-of-memory situation in tuplesort");
        return true;
 
 noalloc:
index 627b281f67b7db774e37c122c7d60b87544255a4..51f474d9c46d33680d9337b49e98e3a73152b535 100644 (file)
@@ -267,7 +267,14 @@ tuplestore_begin_common(int eflags, bool interXact, int maxKBytes)
 
        state->memtupdeleted = 0;
        state->memtupcount = 0;
-       state->memtupsize = 1024;       /* initial guess */
+
+       /*
+        * Initial size of array must be more than ALLOCSET_SEPARATE_THRESHOLD;
+        * see comments in grow_memtuples().
+        */
+       state->memtupsize = Max(16384 / sizeof(void *),
+                                                       ALLOCSET_SEPARATE_THRESHOLD / sizeof(void *) + 1);
+
        state->growmemtuples = true;
        state->memtuples = (void **) palloc(state->memtupsize * sizeof(void *));
 
@@ -641,10 +648,10 @@ grow_memtuples(Tuplestorestate *state)
         * never generate a dangerous request, but to be safe, check explicitly
         * that the array growth fits within availMem.  (We could still cause
         * LACKMEM if the memory chunk overhead associated with the memtuples
-        * array were to increase.  That shouldn't happen with any sane value of
-        * allowedMem, because at any array size large enough to risk LACKMEM,
-        * palloc would be treating both old and new arrays as separate chunks.
-        * But we'll check LACKMEM explicitly below just in case.)
+        * array were to increase.  That shouldn't happen because we chose the
+        * initial array size large enough to ensure that palloc will be treating
+        * both old and new arrays as separate chunks.  But we'll check LACKMEM
+        * explicitly below just in case.)
         */
        if (state->availMem < (int64) ((newmemtupsize - memtupsize) * sizeof(void *)))
                goto noalloc;
@@ -657,7 +664,7 @@ grow_memtuples(Tuplestorestate *state)
                                          state->memtupsize * sizeof(void *));
        USEMEM(state, GetMemoryChunkSpace(state->memtuples));
        if (LACKMEM(state))
-               elog(ERROR, "unexpected out-of-memory situation during sort");
+               elog(ERROR, "unexpected out-of-memory situation in tuplestore");
        return true;
 
 noalloc:
index 9e84d01103f3a9fe53889bc3719889ff047d0e89..f0fe0f449cac4b8e96ac4b6d0927ec593b6830bb 100644 (file)
@@ -150,4 +150,12 @@ extern MemoryContext AllocSetContextCreate(MemoryContext parent,
 #define ALLOCSET_SMALL_INITSIZE  (1 * 1024)
 #define ALLOCSET_SMALL_MAXSIZE  (8 * 1024)
 
+/*
+ * Threshold above which a request in an AllocSet context is certain to be
+ * allocated separately (and thereby have constant allocation overhead).
+ * Few callers should be interested in this, but tuplesort/tuplestore need
+ * to know it.
+ */
+#define ALLOCSET_SEPARATE_THRESHOLD  8192
+
 #endif   /* MEMUTILS_H */