@@ -11,6 +11,7 @@ import (
11
11
"os"
12
12
"path/filepath"
13
13
"reflect"
14
+ "runtime"
14
15
"strings"
15
16
"sync"
16
17
"testing"
@@ -1373,6 +1374,179 @@ func TestDBUnmap(t *testing.T) {
1373
1374
db .DB = nil
1374
1375
}
1375
1376
1377
+ // Convenience function for inserting a bunch of keys with 1000 byte values
1378
+ func fillDBWithKeys (db * btesting.DB , numKeys int ) error {
1379
+ return db .Fill ([]byte ("data" ), 1 , numKeys ,
1380
+ func (tx int , k int ) []byte { return []byte (fmt .Sprintf ("%04d" , k )) },
1381
+ func (tx int , k int ) []byte { return make ([]byte , 1000 ) },
1382
+ )
1383
+ }
1384
+
1385
+ // Creates a new database size, forces a specific allocation size jump, and fills it with the number of keys specified
1386
+ func createFilledDB (t testing.TB , o * bolt.Options , allocSize int , numKeys int ) * btesting.DB {
1387
+ // Open a data file.
1388
+ db := btesting .MustCreateDBWithOption (t , o )
1389
+ db .AllocSize = allocSize
1390
+
1391
+ // Insert a reasonable amount of data below the max size.
1392
+ err := db .Fill ([]byte ("data" ), 1 , numKeys ,
1393
+ func (tx int , k int ) []byte { return []byte (fmt .Sprintf ("%04d" , k )) },
1394
+ func (tx int , k int ) []byte { return make ([]byte , 1000 ) },
1395
+ )
1396
+ if err != nil {
1397
+ t .Fatal (err )
1398
+ }
1399
+ return db
1400
+ }
1401
+
1402
+ // Ensure that a database cannot exceed its maximum size
1403
+ // https://github.com/etcd-io/bbolt/issues/928
1404
+ func TestDB_MaxSizeNotExceeded (t * testing.T ) {
1405
+ testCases := []struct {
1406
+ name string
1407
+ options bolt.Options
1408
+ }{
1409
+ {
1410
+ name : "Standard case" ,
1411
+ options : bolt.Options {
1412
+ MaxSize : 5 * 1024 * 1024 , // 5 MiB
1413
+ PageSize : 4096 ,
1414
+ },
1415
+ },
1416
+ {
1417
+ name : "NoGrowSync" ,
1418
+ options : bolt.Options {
1419
+ MaxSize : 5 * 1024 * 1024 , // 5 MiB
1420
+ PageSize : 4096 ,
1421
+ NoGrowSync : true ,
1422
+ },
1423
+ },
1424
+ }
1425
+
1426
+ for _ , testCase := range testCases {
1427
+ t .Run (testCase .name , func (t * testing.T ) {
1428
+ db := createFilledDB (t ,
1429
+ & testCase .options ,
1430
+ 4 * 1024 * 1024 , // adjust allocation jumps to 4 MiB
1431
+ 2000 ,
1432
+ )
1433
+
1434
+ path := db .Path ()
1435
+
1436
+ // The data file should be 4 MiB now (expanded once from zero).
1437
+ // It should have space for roughly 16 more entries before trying to grow
1438
+ // Keep inserting until grow is required
1439
+ err := fillDBWithKeys (db , 100 )
1440
+ assert .ErrorIs (t , err , berrors .ErrMaxSizeReached )
1441
+
1442
+ newSz := fileSize (path )
1443
+ require .Greater (t , newSz , int64 (0 ), "unexpected new file size: %d" , newSz )
1444
+ assert .LessOrEqual (t , newSz , int64 (db .MaxSize ), "The size of the data file should not exceed db.MaxSize" )
1445
+
1446
+ err = db .Close ()
1447
+ require .NoError (t , err , "Closing the re-opened database should succeed" )
1448
+ })
1449
+ }
1450
+ }
1451
+
1452
+ // Ensure that opening a database that is beyond the maximum size succeeds
1453
+ // The maximum size should only apply to growing the data file
1454
+ // https://github.com/etcd-io/bbolt/issues/928
1455
+ func TestDB_MaxSizeExceededCanOpen (t * testing.T ) {
1456
+ // Open a data file.
1457
+ db := createFilledDB (t , nil , 4 * 1024 * 1024 , 2000 ) // adjust allocation jumps to 4 MiB, fill with 2000, 1KB keys
1458
+ path := db .Path ()
1459
+
1460
+ // Insert a reasonable amount of data below the max size.
1461
+ err := fillDBWithKeys (db , 2000 )
1462
+ require .NoError (t , err , "fillDbWithKeys should succeed" )
1463
+
1464
+ err = db .Close ()
1465
+ require .NoError (t , err , "Close should succeed" )
1466
+
1467
+ // The data file should be 4 MiB now (expanded once from zero).
1468
+ minimumSizeForTest := int64 (1024 * 1024 )
1469
+ newSz := fileSize (path )
1470
+ require .GreaterOrEqual (t , newSz , minimumSizeForTest , "unexpected new file size: %d. Expected at least %d" , newSz , minimumSizeForTest )
1471
+
1472
+ // Now try to re-open the database with an extremely small max size
1473
+ t .Logf ("Reopening bbolt DB at: %s" , path )
1474
+ db , err = btesting .OpenDBWithOption (t , path , & bolt.Options {
1475
+ MaxSize : 1 ,
1476
+ })
1477
+ assert .NoError (t , err , "Should be able to open database bigger than MaxSize" )
1478
+
1479
+ err = db .Close ()
1480
+ require .NoError (t , err , "Closing the re-opened database should succeed" )
1481
+ }
1482
+
1483
+ // Ensure that opening a database that is beyond the maximum size succeeds,
1484
+ // even when InitialMmapSize is above the limit (mmaps should not affect file size)
1485
+ // This test exists for platforms where Truncate should not be called during mmap
1486
+ // https://github.com/etcd-io/bbolt/issues/928
1487
+ func TestDB_MaxSizeExceededCanOpenWithHighMmap (t * testing.T ) {
1488
+ if runtime .GOOS == "windows" {
1489
+ // In Windows, the file must be expanded to the mmap initial size,
1490
+ // so this test doesn't run in Windows.
1491
+ t .SkipNow ()
1492
+ }
1493
+
1494
+ // Open a data file.
1495
+ db := createFilledDB (t , nil , 4 * 1024 * 1024 , 2000 ) // adjust allocation jumps to 4 MiB, fill with 2000 1KB entries
1496
+ path := db .Path ()
1497
+
1498
+ err := db .Close ()
1499
+ require .NoError (t , err , "Close should succeed" )
1500
+
1501
+ // The data file should be 4 MiB now (expanded once from zero).
1502
+ minimumSizeForTest := int64 (1024 * 1024 )
1503
+ newSz := fileSize (path )
1504
+ require .GreaterOrEqual (t , newSz , minimumSizeForTest , "unexpected new file size: %d. Expected at least %d" , newSz , minimumSizeForTest )
1505
+
1506
+ // Now try to re-open the database with an extremely small max size
1507
+ t .Logf ("Reopening bbolt DB at: %s" , path )
1508
+ db , err = btesting .OpenDBWithOption (t , path , & bolt.Options {
1509
+ MaxSize : 1 ,
1510
+ InitialMmapSize : int (minimumSizeForTest ) * 2 ,
1511
+ })
1512
+ assert .NoError (t , err , "Should be able to open database bigger than MaxSize when InitialMmapSize set high" )
1513
+
1514
+ err = db .Close ()
1515
+ require .NoError (t , err , "Closing the re-opened database should succeed" )
1516
+ }
1517
+
1518
+ // Ensure that when InitialMmapSize is above the limit, opening a database
1519
+ // that is beyond the maximum size fails in Windows.
1520
+ // In Windows, the file must be expanded to the mmap initial size.
1521
+ // https://github.com/etcd-io/bbolt/issues/928
1522
+ func TestDB_MaxSizeExceededDoesNotGrow (t * testing.T ) {
1523
+ if runtime .GOOS != "windows" {
1524
+ // This test is only relevant on Windows
1525
+ t .SkipNow ()
1526
+ }
1527
+
1528
+ // Open a data file.
1529
+ db := createFilledDB (t , nil , 4 * 1024 * 1024 , 2000 ) // adjust allocation jumps to 4 MiB, fill with 2000 1KB entries
1530
+ path := db .Path ()
1531
+
1532
+ err := db .Close ()
1533
+ require .NoError (t , err , "Close should succeed" )
1534
+
1535
+ // The data file should be 4 MiB now (expanded once from zero).
1536
+ minimumSizeForTest := int64 (1024 * 1024 )
1537
+ newSz := fileSize (path )
1538
+ assert .GreaterOrEqual (t , newSz , minimumSizeForTest , "unexpected new file size: %d. Expected at least %d" , newSz , minimumSizeForTest )
1539
+
1540
+ // Now try to re-open the database with an extremely small max size and
1541
+ // an initial mmap size to be greater than the actual file size, forcing an illegal grow on open
1542
+ t .Logf ("Reopening bbolt DB at: %s" , path )
1543
+ _ , err = btesting .OpenDBWithOption (t , path , & bolt.Options {
1544
+ MaxSize : 1 ,
1545
+ InitialMmapSize : int (newSz ) * 2 ,
1546
+ })
1547
+ assert .Error (t , err , "Opening the DB with InitialMmapSize > MaxSize should cause an error on Windows" )
1548
+ }
1549
+
1376
1550
func ExampleDB_Update () {
1377
1551
// Open the database.
1378
1552
db , err := bolt .Open (tempfile (), 0600 , nil )
0 commit comments