Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
332 lines
10 KiB
Go
332 lines
10 KiB
Go
package filer
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"sync"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/chunk_cache"
|
|
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
|
)
|
|
|
|
type ChunkGroup struct {
|
|
lookupFn wdclient.LookupFileIdFunctionType
|
|
sections map[SectionIndex]*FileChunkSection
|
|
sectionsLock sync.RWMutex
|
|
readerCache *ReaderCache
|
|
concurrentReaders int
|
|
// cacheInvalidator lets manifest resolution drop stale volume locations, as ReaderCache does for chunk reads
|
|
cacheInvalidator CacheInvalidator
|
|
// manifestCache caches resolved chunk manifest bytes across repeated opens
|
|
// for the same mount. nil for non-mount callers (no caching).
|
|
manifestCache *ChunkManifestCache
|
|
// resolveErr records a manifest resolution failure, guarded by sectionsLock
|
|
resolveErr error
|
|
}
|
|
|
|
// NewChunkGroup creates a ChunkGroup with configurable concurrency.
|
|
// concurrentReaders controls:
|
|
// - Maximum parallel chunk fetches during read operations
|
|
// - Read-ahead prefetch parallelism
|
|
// - Number of concurrent section reads for large files
|
|
// If concurrentReaders <= 0, defaults to 16.
|
|
func NewChunkGroup(lookupFn wdclient.LookupFileIdFunctionType, chunkCache chunk_cache.ChunkCache, chunks []*filer_pb.FileChunk, concurrentReaders int, cacheInvalidator CacheInvalidator, manifestCache *ChunkManifestCache, budgets ...*ReaderCacheBudget) (*ChunkGroup, error) {
|
|
if concurrentReaders <= 0 {
|
|
concurrentReaders = 16
|
|
}
|
|
if concurrentReaders > 128 {
|
|
concurrentReaders = 128 // Cap to prevent excessive goroutine fan-out
|
|
}
|
|
// ReaderCache limit should be at least concurrentReaders to allow parallel prefetching
|
|
readerCacheLimit := concurrentReaders * 2
|
|
if readerCacheLimit < 32 {
|
|
readerCacheLimit = 32
|
|
}
|
|
group := &ChunkGroup{
|
|
lookupFn: lookupFn,
|
|
sections: make(map[SectionIndex]*FileChunkSection),
|
|
readerCache: NewReaderCache(readerCacheLimit, chunkCache, lookupFn, cacheInvalidator, budgets...),
|
|
concurrentReaders: concurrentReaders,
|
|
cacheInvalidator: cacheInvalidator,
|
|
manifestCache: manifestCache,
|
|
}
|
|
|
|
err := group.SetChunks(chunks)
|
|
return group, err
|
|
}
|
|
|
|
// GetPrefetchCount returns the number of chunks to prefetch ahead during sequential reads.
|
|
// This is derived from concurrentReaders to keep the network pipeline full.
|
|
func (group *ChunkGroup) GetPrefetchCount() int {
|
|
// Prefetch at least 1, and scale with concurrency (roughly 1/4 of concurrent readers)
|
|
prefetch := group.concurrentReaders / 4
|
|
if prefetch < 1 {
|
|
prefetch = 1
|
|
}
|
|
if prefetch > 8 {
|
|
prefetch = 8 // Cap at 8 to avoid excessive memory usage
|
|
}
|
|
return prefetch
|
|
}
|
|
|
|
func (group *ChunkGroup) AddChunk(chunk *filer_pb.FileChunk) error {
|
|
|
|
group.sectionsLock.Lock()
|
|
defer group.sectionsLock.Unlock()
|
|
|
|
sectionIndexStart, sectionIndexStop := SectionIndex(chunk.Offset/SectionSize), SectionIndex((chunk.Offset+int64(chunk.Size))/SectionSize)
|
|
for si := sectionIndexStart; si < sectionIndexStop+1; si++ {
|
|
section, found := group.sections[si]
|
|
if !found {
|
|
section = NewFileChunkSection(si)
|
|
group.sections[si] = section
|
|
}
|
|
section.addChunk(chunk)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (group *ChunkGroup) ReadDataAt(ctx context.Context, fileSize int64, buff []byte, offset int64) (n int, tsNs int64, err error) {
|
|
if offset >= fileSize {
|
|
return 0, 0, io.EOF
|
|
}
|
|
|
|
group.sectionsLock.RLock()
|
|
defer group.sectionsLock.RUnlock()
|
|
|
|
if group.resolveErr != nil {
|
|
return 0, 0, group.resolveErr
|
|
}
|
|
|
|
sectionIndexStart, sectionIndexStop := SectionIndex(offset/SectionSize), SectionIndex((offset+int64(len(buff)))/SectionSize)
|
|
numSections := int(sectionIndexStop - sectionIndexStart + 1)
|
|
|
|
// For single section or when concurrency is disabled, use sequential reading
|
|
if numSections <= 1 || group.concurrentReaders <= 1 {
|
|
return group.readDataAtSequential(ctx, fileSize, buff, offset, sectionIndexStart, sectionIndexStop)
|
|
}
|
|
|
|
// For multiple sections, use parallel reading
|
|
return group.readDataAtParallel(ctx, fileSize, buff, offset, sectionIndexStart, sectionIndexStop)
|
|
}
|
|
|
|
// readDataAtSequential reads sections sequentially (original behavior)
|
|
func (group *ChunkGroup) readDataAtSequential(ctx context.Context, fileSize int64, buff []byte, offset int64, sectionIndexStart, sectionIndexStop SectionIndex) (n int, tsNs int64, err error) {
|
|
for si := sectionIndexStart; si < sectionIndexStop+1; si++ {
|
|
section, found := group.sections[si]
|
|
rangeStart, rangeStop := max(offset, int64(si*SectionSize)), min(offset+int64(len(buff)), int64((si+1)*SectionSize))
|
|
if rangeStart >= rangeStop {
|
|
continue
|
|
}
|
|
if !found {
|
|
rangeStop = min(rangeStop, fileSize)
|
|
for i := rangeStart; i < rangeStop; i++ {
|
|
buff[i-offset] = 0
|
|
}
|
|
n = int(int64(n) + rangeStop - rangeStart)
|
|
continue
|
|
}
|
|
xn, xTsNs, xErr := section.readDataAt(ctx, group, fileSize, buff[rangeStart-offset:rangeStop-offset], rangeStart)
|
|
if xErr != nil {
|
|
return n + xn, max(tsNs, xTsNs), xErr
|
|
}
|
|
n += xn
|
|
tsNs = max(tsNs, xTsNs)
|
|
}
|
|
return
|
|
}
|
|
|
|
// sectionReadResult holds the result of a section read operation
|
|
type sectionReadResult struct {
|
|
sectionIndex SectionIndex
|
|
n int
|
|
tsNs int64
|
|
err error
|
|
}
|
|
|
|
// readDataAtParallel reads multiple sections in parallel for better throughput
|
|
func (group *ChunkGroup) readDataAtParallel(ctx context.Context, fileSize int64, buff []byte, offset int64, sectionIndexStart, sectionIndexStop SectionIndex) (n int, tsNs int64, err error) {
|
|
numSections := int(sectionIndexStop - sectionIndexStart + 1)
|
|
|
|
// Limit concurrency to the smaller of concurrentReaders and numSections
|
|
maxConcurrent := group.concurrentReaders
|
|
if numSections < maxConcurrent {
|
|
maxConcurrent = numSections
|
|
}
|
|
|
|
g, gCtx := errgroup.WithContext(ctx)
|
|
g.SetLimit(maxConcurrent)
|
|
|
|
results := make([]sectionReadResult, numSections)
|
|
|
|
for i := 0; i < numSections; i++ {
|
|
si := sectionIndexStart + SectionIndex(i)
|
|
idx := i
|
|
|
|
section, found := group.sections[si]
|
|
rangeStart, rangeStop := max(offset, int64(si*SectionSize)), min(offset+int64(len(buff)), int64((si+1)*SectionSize))
|
|
if rangeStart >= rangeStop {
|
|
continue
|
|
}
|
|
|
|
if !found {
|
|
// Zero-fill missing sections synchronously
|
|
rangeStop = min(rangeStop, fileSize)
|
|
for j := rangeStart; j < rangeStop; j++ {
|
|
buff[j-offset] = 0
|
|
}
|
|
results[idx] = sectionReadResult{
|
|
sectionIndex: si,
|
|
n: int(rangeStop - rangeStart),
|
|
tsNs: 0,
|
|
err: nil,
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Capture variables for closure
|
|
sectionCopy := section
|
|
buffSlice := buff[rangeStart-offset : rangeStop-offset]
|
|
rangeStartCopy := rangeStart
|
|
|
|
g.Go(func() error {
|
|
xn, xTsNs, xErr := sectionCopy.readDataAt(gCtx, group, fileSize, buffSlice, rangeStartCopy)
|
|
results[idx] = sectionReadResult{
|
|
sectionIndex: si,
|
|
n: xn,
|
|
tsNs: xTsNs,
|
|
err: xErr,
|
|
}
|
|
if xErr != nil && xErr != io.EOF {
|
|
return xErr
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Wait for all goroutines to complete
|
|
groupErr := g.Wait()
|
|
|
|
// Aggregate results
|
|
for _, result := range results {
|
|
n += result.n
|
|
tsNs = max(tsNs, result.tsNs)
|
|
// Collect first non-EOF error from results as fallback
|
|
if result.err != nil && result.err != io.EOF && err == nil {
|
|
err = result.err
|
|
}
|
|
}
|
|
|
|
// Prioritize errgroup error (first error that cancelled context)
|
|
if groupErr != nil {
|
|
err = groupErr
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (group *ChunkGroup) SetChunks(chunks []*filer_pb.FileChunk) error {
|
|
group.sectionsLock.Lock()
|
|
defer group.sectionsLock.Unlock()
|
|
|
|
var dataChunks []*filer_pb.FileChunk
|
|
for _, chunk := range chunks {
|
|
|
|
if !chunk.IsChunkManifest {
|
|
dataChunks = append(dataChunks, chunk)
|
|
continue
|
|
}
|
|
|
|
resolvedChunks, err := resolveOneChunkManifest(context.Background(), group.lookupFn, chunk, group.cacheInvalidator, group.manifestCache)
|
|
if err != nil {
|
|
group.resolveErr = err
|
|
return err
|
|
}
|
|
|
|
dataChunks = append(dataChunks, resolvedChunks...)
|
|
}
|
|
|
|
sections := make(map[SectionIndex]*FileChunkSection)
|
|
|
|
for _, chunk := range dataChunks {
|
|
sectionIndexStart, sectionIndexStop := SectionIndex(chunk.Offset/SectionSize), SectionIndex((chunk.Offset+int64(chunk.Size))/SectionSize)
|
|
for si := sectionIndexStart; si < sectionIndexStop+1; si++ {
|
|
section, found := sections[si]
|
|
if !found {
|
|
section = NewFileChunkSection(si)
|
|
sections[si] = section
|
|
}
|
|
section.chunks = append(section.chunks, chunk)
|
|
}
|
|
}
|
|
|
|
group.sections = sections
|
|
group.resolveErr = nil
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
// see weedfs_file_lseek.go
|
|
SEEK_DATA uint32 = 3 // seek to next data after the offset
|
|
// SEEK_HOLE uint32 = 4 // seek to next hole after the offset
|
|
)
|
|
|
|
func (group *ChunkGroup) SearchChunks(ctx context.Context, offset, fileSize int64, whence uint32) (found bool, out int64, err error) {
|
|
group.sectionsLock.RLock()
|
|
defer group.sectionsLock.RUnlock()
|
|
|
|
// the section map is unreliable after a failed manifest resolution
|
|
if group.resolveErr != nil {
|
|
return false, 0, group.resolveErr
|
|
}
|
|
|
|
found, out = group.doSearchChunks(ctx, offset, fileSize, whence)
|
|
return found, out, nil
|
|
}
|
|
|
|
func (group *ChunkGroup) doSearchChunks(ctx context.Context, offset, fileSize int64, whence uint32) (found bool, out int64) {
|
|
|
|
sectionIndex, maxSectionIndex := SectionIndex(offset/SectionSize), SectionIndex(fileSize/SectionSize)
|
|
for si := sectionIndex; si <= maxSectionIndex; si++ {
|
|
sectionStart, sectionStop := sectionBounds(si, fileSize)
|
|
sectionStart = max(offset, sectionStart)
|
|
if sectionStart >= sectionStop {
|
|
continue
|
|
}
|
|
|
|
section, foundSection := group.sections[si]
|
|
if whence == SEEK_DATA {
|
|
if !foundSection {
|
|
continue
|
|
}
|
|
dataStart := section.DataStartOffset(ctx, group, sectionStart, fileSize)
|
|
if dataStart >= sectionStart && dataStart < sectionStop {
|
|
return true, dataStart
|
|
}
|
|
continue
|
|
}
|
|
|
|
// whence == SEEK_HOLE
|
|
if !foundSection {
|
|
return true, sectionStart
|
|
}
|
|
holeStart := section.NextStopOffset(ctx, group, sectionStart, fileSize)
|
|
if holeStart < sectionStop {
|
|
return true, holeStart
|
|
}
|
|
}
|
|
|
|
if whence == SEEK_DATA {
|
|
return false, 0
|
|
}
|
|
return true, fileSize
|
|
}
|
|
|
|
func (group *ChunkGroup) Close() error {
|
|
group.readerCache.destroy()
|
|
return nil
|
|
}
|