@@ -2,11 +2,9 @@ package connmongo
22
33import (
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.
4425func (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
0 commit comments