mirror of
https://github.com/logos-storage/nim-blockstore.git
synced 2026-01-12 12:53:09 +00:00
498 lines
14 KiB
Nim
498 lines
14 KiB
Nim
import std/[os, bitops, memfiles, posix]
|
|
import results
|
|
|
|
import ./errors
|
|
import ./sharding
|
|
import ./cid
|
|
|
|
proc newBlockmap*(size: int): seq[byte] =
|
|
let byteCount = (size + 7) div 8
|
|
newSeq[byte](byteCount)
|
|
|
|
proc blockmapGet*(blockmap: seq[byte], index: int): bool =
|
|
if index < 0:
|
|
return false
|
|
let
|
|
byteIdx = index div 8
|
|
bitIdx = index mod 8
|
|
if byteIdx >= blockmap.len:
|
|
return false
|
|
(blockmap[byteIdx] and (1'u8 shl bitIdx)) != 0
|
|
|
|
proc blockmapSet*(blockmap: var seq[byte], index: int, value: bool) =
|
|
if index < 0:
|
|
return
|
|
let
|
|
byteIdx = index div 8
|
|
bitIdx = index mod 8
|
|
if byteIdx >= blockmap.len:
|
|
return
|
|
if value:
|
|
blockmap[byteIdx] = blockmap[byteIdx] or (1'u8 shl bitIdx)
|
|
else:
|
|
blockmap[byteIdx] = blockmap[byteIdx] and not (1'u8 shl bitIdx)
|
|
|
|
proc blockmapCountOnes*(blockmap: seq[byte]): int =
|
|
result = 0
|
|
for b in blockmap:
|
|
result += countSetBits(b)
|
|
|
|
const
|
|
BlockmapMagic = 0x424D4150'u32
|
|
BlockmapVersion = 1'u8
|
|
BlocksPerChunk* = 1024 * 1024
|
|
GrowthChunk = 1024 * 1024
|
|
HeaderSize = 24
|
|
ChunkEmpty* = 0x00'u8
|
|
ChunkFull* = 0xFF'u8
|
|
ChunkPartial* = 0x01'u8
|
|
|
|
type
|
|
BlockmapBackend* = enum
|
|
bmLevelDb
|
|
bmFile
|
|
|
|
FileBlockmap* = ref object
|
|
path: string
|
|
file: MemFile
|
|
fileSize: int
|
|
maxIndex: uint64
|
|
indexSize: uint32
|
|
readOnly: bool
|
|
|
|
BlockRange* = object
|
|
start*: uint64
|
|
count*: uint64
|
|
|
|
proc headerMaxIndex(mem: pointer): ptr uint64 {.inline.} =
|
|
cast[ptr uint64](cast[uint](mem) + 8)
|
|
|
|
proc headerIndexSize(mem: pointer): ptr uint32 {.inline.} =
|
|
cast[ptr uint32](cast[uint](mem) + 16)
|
|
|
|
proc indexOffset(): int {.inline.} =
|
|
HeaderSize
|
|
|
|
proc bitmapOffset(indexSize: uint32): int {.inline.} =
|
|
HeaderSize + indexSize.int
|
|
|
|
proc chunkIndexPtr(bm: FileBlockmap, chunkIdx: uint32): ptr uint8 {.inline.} =
|
|
if chunkIdx >= bm.indexSize:
|
|
return nil
|
|
cast[ptr uint8](cast[uint](bm.file.mem) + indexOffset().uint + chunkIdx.uint)
|
|
|
|
proc bitmapBytePtr(bm: FileBlockmap, byteIdx: uint64): ptr uint8 {.inline.} =
|
|
let offset = bitmapOffset(bm.indexSize).uint64 + byteIdx
|
|
if offset.int >= bm.fileSize:
|
|
return nil
|
|
cast[ptr uint8](cast[uint](bm.file.mem) + offset.uint)
|
|
|
|
proc getChunkState*(bm: FileBlockmap, chunkIdx: uint32): uint8 =
|
|
let p = bm.chunkIndexPtr(chunkIdx)
|
|
if p == nil:
|
|
return ChunkEmpty
|
|
p[]
|
|
|
|
proc setChunkState(bm: FileBlockmap, chunkIdx: uint32, state: uint8) =
|
|
let p = bm.chunkIndexPtr(chunkIdx)
|
|
if p != nil:
|
|
p[] = state
|
|
|
|
proc neededFileSize(blockIndex: uint64, currentIndexSize: uint32): tuple[fileSize: int, indexSize: uint32] =
|
|
let chunkIdx = (blockIndex div BlocksPerChunk).uint32 + 1
|
|
let newIndexSize = max(currentIndexSize, chunkIdx)
|
|
let byteIdx = blockIndex div 8
|
|
let bitmapEnd = bitmapOffset(newIndexSize) + byteIdx.int + 1
|
|
let fileSize = ((bitmapEnd + GrowthChunk - 1) div GrowthChunk) * GrowthChunk
|
|
(fileSize, newIndexSize)
|
|
|
|
proc growFile(bm: FileBlockmap, newSize: int, newIndexSize: uint32): BResult[void] =
|
|
if bm.readOnly:
|
|
return err(ioError("Cannot grow read-only blockmap"))
|
|
|
|
var oldBitmapData: seq[byte] = @[]
|
|
let oldIndexSize = bm.indexSize
|
|
let oldBitmapOffset = bitmapOffset(oldIndexSize)
|
|
let newBitmapOffset = bitmapOffset(newIndexSize)
|
|
|
|
if newIndexSize > oldIndexSize and oldIndexSize > 0:
|
|
let bitmapSize = bm.fileSize - oldBitmapOffset
|
|
if bitmapSize > 0:
|
|
oldBitmapData = newSeq[byte](bitmapSize)
|
|
copyMem(addr oldBitmapData[0], cast[pointer](cast[uint](bm.file.mem) + oldBitmapOffset.uint), bitmapSize)
|
|
|
|
bm.file.close()
|
|
|
|
try:
|
|
let fd = posix.open(bm.path.cstring, O_RDWR)
|
|
if fd < 0:
|
|
return err(ioError("Failed to open file for truncate"))
|
|
if ftruncate(fd, newSize.Off) != 0:
|
|
discard posix.close(fd)
|
|
return err(ioError("Failed to truncate file"))
|
|
discard posix.close(fd)
|
|
except OSError as e:
|
|
return err(ioError("Failed to grow file: " & e.msg))
|
|
|
|
try:
|
|
bm.file = memfiles.open(bm.path, fmReadWrite, mappedSize = newSize)
|
|
except OSError as e:
|
|
return err(ioError("Failed to remap file: " & e.msg))
|
|
|
|
bm.fileSize = newSize
|
|
headerIndexSize(bm.file.mem)[] = newIndexSize
|
|
|
|
for i in oldIndexSize ..< newIndexSize:
|
|
let p = cast[ptr uint8](cast[uint](bm.file.mem) + indexOffset().uint + i.uint)
|
|
p[] = ChunkEmpty
|
|
|
|
if oldBitmapData.len > 0:
|
|
copyMem(cast[pointer](cast[uint](bm.file.mem) + newBitmapOffset.uint), addr oldBitmapData[0], oldBitmapData.len)
|
|
if newBitmapOffset > oldBitmapOffset:
|
|
let gapSize = min(newBitmapOffset - oldBitmapOffset, oldBitmapData.len)
|
|
zeroMem(cast[pointer](cast[uint](bm.file.mem) + oldBitmapOffset.uint), gapSize)
|
|
|
|
bm.indexSize = newIndexSize
|
|
ok()
|
|
|
|
proc ensureCapacity(bm: FileBlockmap, blockIndex: uint64): BResult[void] =
|
|
let (neededSize, neededIndexSize) = neededFileSize(blockIndex, bm.indexSize)
|
|
if neededSize <= bm.fileSize and neededIndexSize <= bm.indexSize:
|
|
return ok()
|
|
?bm.growFile(max(neededSize, bm.fileSize), max(neededIndexSize, bm.indexSize))
|
|
ok()
|
|
|
|
proc get*(bm: FileBlockmap, index: uint64): bool {.inline.} =
|
|
if index >= bm.maxIndex:
|
|
return false
|
|
|
|
let chunkIdx = (index div BlocksPerChunk).uint32
|
|
let chunkState = bm.getChunkState(chunkIdx)
|
|
|
|
if chunkState == ChunkEmpty:
|
|
return false
|
|
if chunkState == ChunkFull:
|
|
return true
|
|
|
|
let byteIdx = index div 8
|
|
let bitIdx = index mod 8
|
|
let p = bm.bitmapBytePtr(byteIdx)
|
|
if p == nil:
|
|
return false
|
|
(p[] and (1'u8 shl bitIdx)) != 0
|
|
|
|
proc set*(bm: FileBlockmap, index: uint64): BResult[void] =
|
|
if bm.readOnly:
|
|
return err(ioError("Cannot write to read-only blockmap"))
|
|
|
|
?bm.ensureCapacity(index)
|
|
|
|
let chunkIdx = (index div BlocksPerChunk).uint32
|
|
let chunkState = bm.getChunkState(chunkIdx)
|
|
|
|
if chunkState == ChunkFull:
|
|
return ok()
|
|
|
|
let byteIdx = index div 8
|
|
let bitIdx = index mod 8
|
|
let p = bm.bitmapBytePtr(byteIdx)
|
|
if p != nil:
|
|
p[] = p[] or (1'u8 shl bitIdx)
|
|
|
|
if chunkState == ChunkEmpty:
|
|
bm.setChunkState(chunkIdx, ChunkPartial)
|
|
|
|
if index + 1 > bm.maxIndex:
|
|
bm.maxIndex = index + 1
|
|
headerMaxIndex(bm.file.mem)[] = bm.maxIndex
|
|
|
|
ok()
|
|
|
|
proc clear*(bm: FileBlockmap, index: uint64): BResult[void] =
|
|
if bm.readOnly:
|
|
return err(ioError("Cannot write to read-only blockmap"))
|
|
|
|
if index >= bm.maxIndex:
|
|
return ok()
|
|
|
|
let chunkIdx = (index div BlocksPerChunk).uint32
|
|
let chunkState = bm.getChunkState(chunkIdx)
|
|
|
|
if chunkState == ChunkEmpty:
|
|
return ok()
|
|
|
|
let byteIdx = index div 8
|
|
let bitIdx = index mod 8
|
|
let p = bm.bitmapBytePtr(byteIdx)
|
|
if p != nil:
|
|
p[] = p[] and not (1'u8 shl bitIdx)
|
|
|
|
if chunkState == ChunkFull:
|
|
bm.setChunkState(chunkIdx, ChunkPartial)
|
|
|
|
ok()
|
|
|
|
proc countChunkBits(bm: FileBlockmap, chunkIdx: uint32): int =
|
|
let startBlock = chunkIdx.uint64 * BlocksPerChunk
|
|
let endBlock = min(startBlock + BlocksPerChunk, bm.maxIndex)
|
|
if startBlock >= endBlock:
|
|
return 0
|
|
|
|
let startByte = startBlock div 8
|
|
let endByte = (endBlock + 7) div 8
|
|
|
|
result = 0
|
|
for i in startByte ..< endByte:
|
|
let p = bm.bitmapBytePtr(i)
|
|
if p != nil:
|
|
result += countSetBits(p[])
|
|
|
|
proc compactIndex*(bm: FileBlockmap) =
|
|
if bm.readOnly:
|
|
return
|
|
|
|
for i in 0'u32 ..< bm.indexSize:
|
|
let state = bm.getChunkState(i)
|
|
if state == ChunkPartial:
|
|
let bits = bm.countChunkBits(i)
|
|
let startBlock = i.uint64 * BlocksPerChunk
|
|
let blocksInChunk = min(BlocksPerChunk.uint64, bm.maxIndex - startBlock).int
|
|
|
|
if bits == 0:
|
|
bm.setChunkState(i, ChunkEmpty)
|
|
elif bits == blocksInChunk:
|
|
bm.setChunkState(i, ChunkFull)
|
|
|
|
proc countOnes*(bm: FileBlockmap): uint64 =
|
|
result = 0
|
|
for i in 0'u32 ..< bm.indexSize:
|
|
let state = bm.getChunkState(i)
|
|
case state
|
|
of ChunkEmpty:
|
|
discard
|
|
of ChunkFull:
|
|
let startBlock = i.uint64 * BlocksPerChunk
|
|
result += min(BlocksPerChunk.uint64, bm.maxIndex - startBlock)
|
|
else:
|
|
result += bm.countChunkBits(i).uint64
|
|
|
|
proc isComplete*(bm: FileBlockmap, totalBlocks: uint64): bool =
|
|
if bm.maxIndex < totalBlocks:
|
|
return false
|
|
let neededChunks = ((totalBlocks + BlocksPerChunk - 1) div BlocksPerChunk).uint32
|
|
for i in 0'u32 ..< neededChunks:
|
|
if bm.getChunkState(i) != ChunkFull:
|
|
return false
|
|
true
|
|
|
|
proc isEmpty*(bm: FileBlockmap): bool =
|
|
for i in 0'u32 ..< bm.indexSize:
|
|
if bm.getChunkState(i) != ChunkEmpty:
|
|
return false
|
|
true
|
|
|
|
proc maxBlockIndex*(bm: FileBlockmap): uint64 =
|
|
bm.maxIndex
|
|
|
|
proc toRanges*(bm: FileBlockmap): seq[BlockRange] =
|
|
result = @[]
|
|
if bm.indexSize == 0:
|
|
return
|
|
|
|
var currentStart: uint64 = 0
|
|
var inRange = false
|
|
|
|
for i in 0'u32 ..< bm.indexSize:
|
|
let state = bm.getChunkState(i)
|
|
let chunkStart = i.uint64 * BlocksPerChunk
|
|
let chunkEnd = min(chunkStart + BlocksPerChunk, bm.maxIndex)
|
|
|
|
case state
|
|
of ChunkFull:
|
|
if not inRange:
|
|
currentStart = chunkStart
|
|
inRange = true
|
|
|
|
if i == bm.indexSize - 1 or bm.getChunkState(i + 1) != ChunkFull:
|
|
result.add(BlockRange(start: currentStart, count: chunkEnd - currentStart))
|
|
inRange = false
|
|
|
|
of ChunkEmpty:
|
|
if inRange:
|
|
result.add(BlockRange(start: currentStart, count: chunkStart - currentStart))
|
|
inRange = false
|
|
|
|
of ChunkPartial:
|
|
if inRange:
|
|
result.add(BlockRange(start: currentStart, count: chunkStart - currentStart))
|
|
inRange = false
|
|
|
|
var j = chunkStart
|
|
while j < chunkEnd:
|
|
if bm.get(j):
|
|
let rangeStart = j
|
|
while j < chunkEnd and bm.get(j):
|
|
inc j
|
|
result.add(BlockRange(start: rangeStart, count: j - rangeStart))
|
|
else:
|
|
inc j
|
|
|
|
else:
|
|
discard
|
|
|
|
proc flush*(bm: FileBlockmap) =
|
|
if not bm.readOnly:
|
|
bm.file.flush()
|
|
|
|
proc close*(bm: FileBlockmap) =
|
|
if bm.file.mem != nil:
|
|
bm.flush()
|
|
bm.file.close()
|
|
|
|
proc setAll*(bm: FileBlockmap, totalBlocks: uint64): BResult[void] =
|
|
if bm.readOnly:
|
|
return err(ioError("Cannot write to read-only blockmap"))
|
|
|
|
if totalBlocks == 0:
|
|
return ok()
|
|
|
|
?bm.ensureCapacity(totalBlocks - 1)
|
|
|
|
let fullBytes = totalBlocks div 8
|
|
let remainderBits = totalBlocks mod 8
|
|
|
|
for i in 0'u64 ..< fullBytes:
|
|
let p = bm.bitmapBytePtr(i)
|
|
if p != nil:
|
|
p[] = 0xFF'u8
|
|
|
|
if remainderBits > 0:
|
|
let p = bm.bitmapBytePtr(fullBytes)
|
|
if p != nil:
|
|
p[] = (1'u8 shl remainderBits) - 1
|
|
|
|
bm.maxIndex = totalBlocks
|
|
headerMaxIndex(bm.file.mem)[] = totalBlocks
|
|
|
|
let chunkCount = ((totalBlocks + BlocksPerChunk - 1) div BlocksPerChunk).uint32
|
|
for i in 0'u32 ..< chunkCount:
|
|
bm.setChunkState(i, ChunkFull)
|
|
|
|
ok()
|
|
|
|
proc finalize*(bm: FileBlockmap, totalBlocks: uint64): BResult[void] =
|
|
if bm.readOnly:
|
|
return ok()
|
|
|
|
if totalBlocks > bm.maxIndex:
|
|
bm.maxIndex = totalBlocks
|
|
headerMaxIndex(bm.file.mem)[] = totalBlocks
|
|
|
|
bm.compactIndex()
|
|
bm.flush()
|
|
ok()
|
|
|
|
proc newFileBlockmap*(path: string, forWriting: bool = true): BResult[FileBlockmap] =
|
|
let parentDir = parentDir(path)
|
|
if not dirExists(parentDir):
|
|
try:
|
|
createDir(parentDir)
|
|
except OSError as e:
|
|
return err(ioError("Failed to create directory: " & e.msg))
|
|
|
|
var isNew = not fileExists(path)
|
|
|
|
if isNew and not forWriting:
|
|
return err(ioError("Blockmap file does not exist: " & path))
|
|
|
|
var initialSize = HeaderSize + GrowthChunk
|
|
|
|
if isNew:
|
|
try:
|
|
let fd = posix.open(path.cstring, O_RDWR or O_CREAT, 0o644)
|
|
if fd < 0:
|
|
return err(ioError("Failed to create blockmap file"))
|
|
if ftruncate(fd, initialSize.Off) != 0:
|
|
discard posix.close(fd)
|
|
return err(ioError("Failed to set initial file size"))
|
|
discard posix.close(fd)
|
|
except OSError as e:
|
|
return err(ioError("Failed to create blockmap file: " & e.msg))
|
|
else:
|
|
try:
|
|
initialSize = getFileSize(path).int
|
|
except OSError as e:
|
|
return err(ioError("Failed to get file size: " & e.msg))
|
|
|
|
let mode = if forWriting: fmReadWrite else: fmRead
|
|
var mf: MemFile
|
|
try:
|
|
mf = memfiles.open(path, mode, mappedSize = initialSize)
|
|
except OSError as e:
|
|
return err(ioError("Failed to mmap blockmap: " & e.msg))
|
|
|
|
var bm = FileBlockmap(
|
|
path: path,
|
|
file: mf,
|
|
fileSize: initialSize,
|
|
maxIndex: 0,
|
|
indexSize: 0,
|
|
readOnly: not forWriting
|
|
)
|
|
|
|
if isNew:
|
|
let header = cast[ptr uint32](mf.mem)
|
|
header[] = BlockmapMagic
|
|
cast[ptr uint8](cast[uint](mf.mem) + 4)[] = BlockmapVersion
|
|
headerMaxIndex(mf.mem)[] = 0
|
|
headerIndexSize(mf.mem)[] = 0
|
|
else:
|
|
let magic = cast[ptr uint32](mf.mem)[]
|
|
if magic != BlockmapMagic:
|
|
mf.close()
|
|
return err(ioError("Invalid blockmap magic"))
|
|
let version = cast[ptr uint8](cast[uint](mf.mem) + 4)[]
|
|
if version != BlockmapVersion:
|
|
mf.close()
|
|
return err(ioError("Unsupported blockmap version"))
|
|
bm.maxIndex = headerMaxIndex(mf.mem)[]
|
|
bm.indexSize = headerIndexSize(mf.mem)[]
|
|
|
|
ok(bm)
|
|
|
|
proc getBlockmapPath*(blockmapsDir: string, treeCid: Cid): string =
|
|
getShardedPath(blockmapsDir, treeCid, ".blkmap")
|
|
|
|
proc getBlockmapPathStr*(blockmapsDir: string, treeCidStr: string): string =
|
|
getShardedPathStr(blockmapsDir, treeCidStr, ".blkmap")
|
|
|
|
proc toSeqByte*(bm: FileBlockmap): seq[byte] =
|
|
let bitmapSize = (bm.maxIndex + 7) div 8
|
|
result = newSeq[byte](bitmapSize.int)
|
|
for i in 0'u64 ..< bitmapSize:
|
|
let p = bm.bitmapBytePtr(i)
|
|
if p != nil:
|
|
result[i.int] = p[]
|
|
|
|
proc fromSeqByte*(bm: FileBlockmap, data: seq[byte]): BResult[void] =
|
|
if bm.readOnly:
|
|
return err(ioError("Cannot write to read-only blockmap"))
|
|
|
|
let maxIndex = data.len.uint64 * 8
|
|
?bm.ensureCapacity(maxIndex - 1)
|
|
|
|
for i in 0'u64 ..< data.len.uint64:
|
|
let p = bm.bitmapBytePtr(i)
|
|
if p != nil:
|
|
p[] = data[i.int]
|
|
|
|
bm.maxIndex = maxIndex
|
|
headerMaxIndex(bm.file.mem)[] = maxIndex
|
|
|
|
let chunkCount = ((maxIndex + BlocksPerChunk - 1) div BlocksPerChunk).uint32
|
|
for i in 0'u32 ..< chunkCount:
|
|
bm.setChunkState(i, ChunkPartial)
|
|
bm.compactIndex()
|
|
|
|
ok()
|