Skip to content

Commit 69ae052

Browse files
committed
Fix unbounded sorted-query allocation
Start the top-k heap with a small capacity so an effectively unlimited wire-protocol limit does not reserve gigabytes before reading any documents. Add regression coverage for the maximum default limit and document the fix. Thanks to jeremy-arsia and xet7 ! Fixes wekan/wekan#6666,
1 parent 0331895 commit 69ae052

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
<!-- markdownlint-disable MD024 MD034 -->
44

5+
## Upcoming FerretDB release
6+
7+
### Fixed 🐛
8+
9+
- **Sorted queries with an effectively unlimited result limit no longer reserve
10+
memory for every possible result before reading the first document.** The
11+
bounded top-k heap now starts with a small capacity and grows only for rows
12+
that actually exist, preventing ordinary client queries from attempting a
13+
multi-gigabyte allocation and taking down the database. Positive coverage
14+
verifies correct ordering with the wire protocol's maximum default limit,
15+
while the existing finite-limit test retains bounded top-k behavior by
16+
@xet7. Thanks to jeremy-arsia, Heart1010 and xet7.
17+
518
## [v1.64.0](https://github.com/wekan/FerretDB/releases/tag/v1.64.0) (2026-08-30)
619

720
### Fixed 🐛

internal/handler/common/sort_iterator.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ import (
2424
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
2525
)
2626

27+
// sortLimitInitialCapacity keeps an effectively unlimited wire-protocol limit
28+
// from becoming a multi-gigabyte allocation before the first document is read.
29+
// The slice still grows normally when the result set actually contains more.
30+
const sortLimitInitialCapacity int64 = 1024
31+
2732
// SortIterator returns an iterator of sorted documents.
2833
// It will be added to the given closer.
2934
//
@@ -62,7 +67,8 @@ func SortLimitIterator(iter types.DocumentsIterator, closer *iterator.MultiClose
6267
return nil, lazyerrors.Error(err)
6368
}
6469

65-
h := &documentsMaxHeap{sorts: sorts, docs: make([]*types.Document, 0, keep)}
70+
capacity := min(keep, sortLimitInitialCapacity)
71+
h := &documentsMaxHeap{sorts: sorts, docs: make([]*types.Document, 0, capacity)}
6672
heap.Init(h)
6773
for {
6874
_, doc, nextErr := iter.Next()

internal/handler/common/sort_limit_iterator_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package common
77

88
import (
9+
"math"
910
"testing"
1011

1112
"github.com/stretchr/testify/require"
@@ -42,3 +43,25 @@ func TestSortLimitIterator(t *testing.T) {
4243
must.NotFail(actual[2].Get("_id")).(string),
4344
})
4445
}
46+
47+
func TestSortLimitIteratorEffectivelyUnlimited(t *testing.T) {
48+
t.Parallel()
49+
50+
docs := []*types.Document{
51+
must.NotFail(types.NewDocument("_id", "two", "sort", int32(2))),
52+
must.NotFail(types.NewDocument("_id", "one", "sort", int32(1))),
53+
}
54+
sortDoc := must.NotFail(types.NewDocument("sort", int32(1)))
55+
closer := iterator.NewMultiCloser()
56+
defer closer.Close()
57+
source := iterator.Values(iterator.ForSlice(docs))
58+
closer.Add(source)
59+
60+
limited, err := SortLimitIterator(source, closer, sortDoc, math.MaxInt32)
61+
require.NoError(t, err)
62+
actual, err := iterator.ConsumeValues(limited)
63+
require.NoError(t, err)
64+
require.Len(t, actual, 2)
65+
require.Equal(t, "one", must.NotFail(actual[0].Get("_id")))
66+
require.Equal(t, "two", must.NotFail(actual[1].Get("_id")))
67+
}

0 commit comments

Comments
 (0)