Skip to content

Commit a40374b

Browse files
committed
using the full 12 bytes instead of just first 4 bytes of objectID for partitioning
1 parent e623620 commit a40374b

2 files changed

Lines changed: 50 additions & 41 deletions

File tree

flow/connectors/mongo/qrep_partition.go

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ package connmongo
22

33
import (
44
"context"
5-
"encoding/binary"
65
"fmt"
76
"log/slog"
8-
"math"
9-
"time"
7+
"math/big"
108

119
"github.com/google/uuid"
1210
"go.mongodb.org/mongo-driver/v2/bson"
@@ -18,29 +16,12 @@ import (
1816
"github.com/PeerDB-io/peerdb/flow/shared"
1917
)
2018

21-
// minObjectIDForTimestamp creates an ObjectID with the given timestamp and
22-
// all trailing bytes set to 0x00, producing the smallest possible ObjectID
23-
// for that second.
24-
func minObjectIDForTimestamp(t time.Time) bson.ObjectID {
25-
var oid [12]byte
26-
binary.BigEndian.PutUint32(oid[0:4], uint32(t.Unix()))
27-
return oid
28-
}
29-
30-
// maxObjectIDForTimestamp creates an ObjectID with the given timestamp and
31-
// all trailing bytes set to 0xFF (max unsigned 64-bit value), producing
32-
// the largest possible ObjectID for that second.
33-
func maxObjectIDForTimestamp(t time.Time) bson.ObjectID {
34-
var oid [12]byte
35-
binary.BigEndian.PutUint32(oid[0:4], uint32(t.Unix()))
36-
binary.BigEndian.PutUint64(oid[4:], math.MaxUint64)
37-
return oid
38-
}
39-
4019
// minMaxPartitions creates partitions by querying only the min and max _id
4120
// from the collection (leveraging the default _id index), then uniformly
42-
// dividing the timestamp range encoded in the ObjectIDs. Note that partition
43-
// may be skewed if document insertion rate varies significantly over time.
21+
// dividing the ObjectID range using integer arithmetic on the full 12-byte
22+
// value. Since the first 4-byte timestamp is the most significant component,
23+
// this naturally partitions by time while also provides best-effort handling
24+
// of edge cases where records are all inserted in the same second.
4425
func (c *MongoConnector) minMaxPartitions(
4526
ctx context.Context,
4627
collection *mongo.Collection,
@@ -61,34 +42,44 @@ func (c *MongoConnector) minMaxPartitions(
6142

6243
minID := minRaw.ObjectID()
6344
maxID := maxRaw.ObjectID()
64-
minTs := minID.Timestamp()
65-
maxTs := maxID.Timestamp()
66-
tsRange := maxTs.Unix() - minTs.Unix() + 1
67-
if tsRange <= 1 {
68-
c.logger.Info("[mongo] min/max timestamps are equal, falling back to full table partition")
45+
46+
minInt := new(big.Int).SetBytes(minID[:])
47+
maxInt := new(big.Int).SetBytes(maxID[:])
48+
intRange := new(big.Int).Sub(maxInt, minInt)
49+
if intRange.Sign() <= 0 {
50+
c.logger.Info("[mongo] min/max ObjectIDs are equal, falling back to full table partition")
6951
return fullTablePartition, nil
7052
}
71-
// Cap partitions to the timestamp range in seconds so we don't create additional
72-
// empty partitions unnecessarily when docs span fewer seconds than partitions
73-
numPartitions = min(numPartitions, tsRange)
7453

75-
c.logger.Info("[mongo] using min/max ObjectID timestamp partitioning",
76-
slog.Time("minTimestamp", minTs),
77-
slog.Time("maxTimestamp", maxTs),
54+
c.logger.Info("[mongo] using min/max ObjectID partitioning",
55+
slog.String("minID", minID.Hex()),
56+
slog.String("maxID", maxID.Hex()),
7857
slog.Int64("numPartitions", numPartitions))
7958

80-
secondsPerPartition := shared.DivCeil(tsRange, numPartitions)
59+
step := shared.BigIntDivCeil(intRange, big.NewInt(numPartitions))
60+
8161
partitions := make([]*protos.QRepPartition, 0, numPartitions)
82-
for i := range numPartitions {
83-
start := minObjectIDForTimestamp(time.Unix(minTs.Unix()+secondsPerPartition*i, 0))
84-
end := maxObjectIDForTimestamp(time.Unix(minTs.Unix()+secondsPerPartition*(i+1)-1, 0))
62+
for start := new(big.Int).Set(minInt); start.Cmp(maxInt) <= 0; start.Add(start, step) {
63+
end := new(big.Int).Add(start, step)
64+
end.Sub(end, big.NewInt(1))
65+
if end.Cmp(maxInt) > 0 {
66+
end.Set(maxInt)
67+
}
68+
startObjectID, err := bigIntToObjectID(start)
69+
if err != nil {
70+
return nil, err
71+
}
72+
endObjectID, err := bigIntToObjectID(end)
73+
if err != nil {
74+
return nil, err
75+
}
8576
partitions = append(partitions, &protos.QRepPartition{
8677
PartitionId: uuid.NewString(),
8778
Range: &protos.PartitionRange{
8879
Range: &protos.PartitionRange_ObjectIdRange{
8980
ObjectIdRange: &protos.ObjectIdPartitionRange{
90-
Start: start.Hex(),
91-
End: end.Hex(),
81+
Start: startObjectID.Hex(),
82+
End: endObjectID.Hex(),
9283
},
9384
},
9485
},
@@ -99,6 +90,16 @@ func (c *MongoConnector) minMaxPartitions(
9990
return partitions, nil
10091
}
10192

93+
// bigIntToObjectID converts a big.Int back to a 12-byte ObjectID.
94+
func bigIntToObjectID(n *big.Int) (bson.ObjectID, error) {
95+
var oid bson.ObjectID
96+
if n.BitLen() > 96 {
97+
return oid, fmt.Errorf("big.Int value exceeds 96 bits (ObjectID size)")
98+
}
99+
n.FillBytes(oid[:])
100+
return oid, nil
101+
}
102+
102103
// findMinMaxObjectIDs returns the min and max object IDs from the collection.
103104
// The two boundary queries are not wrapped in a snapshot session: a concurrent
104105
// insert between them may shift the true min/max, but this is safe because the

flow/shared/math.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
package shared
22

33
import (
4+
"math/big"
5+
46
"golang.org/x/exp/constraints"
57
)
68

79
func DivCeil[T constraints.Integer](x, y T) T {
810
return (x + y - 1) / y
911
}
1012

13+
// BigIntDivCeil returns ceil(x / y) for big.Int values.
14+
func BigIntDivCeil(x, y *big.Int) *big.Int {
15+
result := new(big.Int).Add(x, new(big.Int).Sub(y, big.NewInt(1)))
16+
return result.Div(result, y)
17+
}
18+
1119
// AdjustedPartitions represents the adjusted partitioning parameters
1220
type AdjustedPartitions struct {
1321
AdjustedNumPartitions int64

0 commit comments

Comments
 (0)