snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
52
weed/mount/page_writer/activity_score.go
Normal file
52
weed/mount/page_writer/activity_score.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ActivityScore struct {
|
||||
lastActiveTsNs int64 // atomic
|
||||
decayedActivenessScore int64 // atomic
|
||||
}
|
||||
|
||||
func NewActivityScore() *ActivityScore {
|
||||
return &ActivityScore{}
|
||||
}
|
||||
|
||||
func (as *ActivityScore) MarkRead() {
|
||||
now := time.Now().UnixNano()
|
||||
last := atomic.SwapInt64(&as.lastActiveTsNs, now)
|
||||
deltaTime := (now - last) >> 30 // about number of seconds
|
||||
for {
|
||||
old := atomic.LoadInt64(&as.decayedActivenessScore)
|
||||
score := old>>deltaTime + 256
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
if atomic.CompareAndSwapInt64(&as.decayedActivenessScore, old, score) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (as *ActivityScore) MarkWrite() {
|
||||
now := time.Now().UnixNano()
|
||||
last := atomic.SwapInt64(&as.lastActiveTsNs, now)
|
||||
deltaTime := (now - last) >> 30 // about number of seconds
|
||||
for {
|
||||
old := atomic.LoadInt64(&as.decayedActivenessScore)
|
||||
score := old>>deltaTime + 1024
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
if atomic.CompareAndSwapInt64(&as.decayedActivenessScore, old, score) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (as *ActivityScore) ActivityScore() int64 {
|
||||
deltaTime := (time.Now().UnixNano() - atomic.LoadInt64(&as.lastActiveTsNs)) >> 30
|
||||
return atomic.LoadInt64(&as.decayedActivenessScore) >> deltaTime
|
||||
}
|
||||
165
weed/mount/page_writer/chunk_interval_list.go
Normal file
165
weed/mount/page_writer/chunk_interval_list.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
// ChunkWrittenInterval mark one written interval within one page chunk
|
||||
type ChunkWrittenInterval struct {
|
||||
StartOffset int64
|
||||
stopOffset int64
|
||||
TsNs int64
|
||||
prev *ChunkWrittenInterval
|
||||
next *ChunkWrittenInterval
|
||||
}
|
||||
|
||||
func (interval *ChunkWrittenInterval) Size() int64 {
|
||||
return interval.stopOffset - interval.StartOffset
|
||||
}
|
||||
|
||||
func (interval *ChunkWrittenInterval) isComplete(chunkSize int64) bool {
|
||||
return interval.stopOffset-interval.StartOffset == chunkSize
|
||||
}
|
||||
|
||||
// ChunkWrittenIntervalList mark written intervals within one page chunk
|
||||
type ChunkWrittenIntervalList struct {
|
||||
head *ChunkWrittenInterval
|
||||
tail *ChunkWrittenInterval
|
||||
}
|
||||
|
||||
func newChunkWrittenIntervalList() *ChunkWrittenIntervalList {
|
||||
list := &ChunkWrittenIntervalList{
|
||||
head: &ChunkWrittenInterval{
|
||||
StartOffset: -1,
|
||||
stopOffset: -1,
|
||||
},
|
||||
tail: &ChunkWrittenInterval{
|
||||
StartOffset: math.MaxInt64,
|
||||
stopOffset: math.MaxInt64,
|
||||
},
|
||||
}
|
||||
list.head.next = list.tail
|
||||
list.tail.prev = list.head
|
||||
return list
|
||||
}
|
||||
|
||||
func (list *ChunkWrittenIntervalList) MarkWritten(startOffset, stopOffset, tsNs int64) {
|
||||
if startOffset >= stopOffset {
|
||||
return
|
||||
}
|
||||
interval := &ChunkWrittenInterval{
|
||||
StartOffset: startOffset,
|
||||
stopOffset: stopOffset,
|
||||
TsNs: tsNs,
|
||||
}
|
||||
list.addInterval(interval)
|
||||
}
|
||||
|
||||
// IsComplete reports whether every byte of [0, chunkSize) has been
|
||||
// written, possibly via multiple adjacent or overlapping intervals.
|
||||
// addInterval does not merge adjacent intervals — a chunk filled by
|
||||
// two 1 MiB writes ends up as {[0,1M], [1M,2M]}, not one [0,2M] —
|
||||
// so checking list.size()==1 misses the "filled by adjacent writes"
|
||||
// case, leaving the chunk pinned in writableChunks even though all
|
||||
// its bytes are present. That latent bug became a hard deadlock once
|
||||
// -writeBufferSizeMB started reserving a global slot per writable
|
||||
// chunk: the chunks never got sealed, no uploader ran, no slot was
|
||||
// ever released, and the FUSE writer blocked in Reserve forever
|
||||
// (seaweedfs issue #8777 / PR #9066). Walking the list and tracking
|
||||
// the furthest covered offset detects adjacency correctly.
|
||||
func (list *ChunkWrittenIntervalList) IsComplete(chunkSize int64) bool {
|
||||
var covered int64
|
||||
for t := list.head.next; t != list.tail; t = t.next {
|
||||
if t.StartOffset > covered {
|
||||
return false // gap before this interval
|
||||
}
|
||||
if t.stopOffset > covered {
|
||||
covered = t.stopOffset
|
||||
}
|
||||
}
|
||||
return covered >= chunkSize
|
||||
}
|
||||
func (list *ChunkWrittenIntervalList) WrittenSize() (writtenByteCount int64) {
|
||||
for t := list.head; t != nil; t = t.next {
|
||||
writtenByteCount += t.Size()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// IsContiguouslyWritten reports whether the written bytes form one
|
||||
// unbroken run starting at offset 0. Pressure-driven sealing uses this
|
||||
// to avoid racing in-flight FUSE writeback on a gap range — sealing a
|
||||
// gappy chunk would emit volume chunks with no coverage for the gap and
|
||||
// reads would silently zero-fill it (issue #9330).
|
||||
func (list *ChunkWrittenIntervalList) IsContiguouslyWritten() bool {
|
||||
first := list.head.next
|
||||
if first == list.tail || first.StartOffset != 0 {
|
||||
return false
|
||||
}
|
||||
for t := first; t.next != list.tail; t = t.next {
|
||||
if t.stopOffset != t.next.StartOffset {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (list *ChunkWrittenIntervalList) addInterval(interval *ChunkWrittenInterval) {
|
||||
|
||||
//t := list.head
|
||||
//for ; t.next != nil; t = t.next {
|
||||
// if t.TsNs > interval.TsNs {
|
||||
// println("writes is out of order", t.TsNs-interval.TsNs, "ns")
|
||||
// }
|
||||
//}
|
||||
|
||||
p := list.head
|
||||
for ; p.next != nil && p.next.stopOffset <= interval.StartOffset; p = p.next {
|
||||
}
|
||||
q := list.tail
|
||||
for ; q.prev != nil && q.prev.StartOffset >= interval.stopOffset; q = q.prev {
|
||||
}
|
||||
|
||||
// left side
|
||||
// interval after p.next start
|
||||
if p.next.StartOffset < interval.StartOffset {
|
||||
t := &ChunkWrittenInterval{
|
||||
StartOffset: p.next.StartOffset,
|
||||
stopOffset: interval.StartOffset,
|
||||
TsNs: p.next.TsNs,
|
||||
}
|
||||
p.next = t
|
||||
t.prev = p
|
||||
t.next = interval
|
||||
interval.prev = t
|
||||
} else {
|
||||
p.next = interval
|
||||
interval.prev = p
|
||||
}
|
||||
|
||||
// right side
|
||||
// interval ends before p.prev
|
||||
if interval.stopOffset < q.prev.stopOffset {
|
||||
t := &ChunkWrittenInterval{
|
||||
StartOffset: interval.stopOffset,
|
||||
stopOffset: q.prev.stopOffset,
|
||||
TsNs: q.prev.TsNs,
|
||||
}
|
||||
q.prev = t
|
||||
t.next = q
|
||||
interval.next = t
|
||||
t.prev = interval
|
||||
} else {
|
||||
q.prev = interval
|
||||
interval.next = q
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (list *ChunkWrittenIntervalList) size() int {
|
||||
var count int
|
||||
for t := list.head; t != nil; t = t.next {
|
||||
count++
|
||||
}
|
||||
return count - 2
|
||||
}
|
||||
189
weed/mount/page_writer/chunk_interval_list_test.go
Normal file
189
weed/mount/page_writer/chunk_interval_list_test.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_PageChunkWrittenIntervalList(t *testing.T) {
|
||||
list := newChunkWrittenIntervalList()
|
||||
|
||||
assert.Equal(t, 0, list.size(), "empty list")
|
||||
|
||||
list.MarkWritten(0, 5, 1)
|
||||
assert.Equal(t, 1, list.size(), "one interval")
|
||||
|
||||
list.MarkWritten(0, 5, 2)
|
||||
assert.Equal(t, 1, list.size(), "duplicated interval2")
|
||||
|
||||
list.MarkWritten(95, 100, 3)
|
||||
assert.Equal(t, 2, list.size(), "two intervals")
|
||||
|
||||
list.MarkWritten(50, 60, 4)
|
||||
assert.Equal(t, 3, list.size(), "three intervals")
|
||||
|
||||
list.MarkWritten(50, 55, 5)
|
||||
assert.Equal(t, 4, list.size(), "three intervals merge")
|
||||
|
||||
list.MarkWritten(40, 50, 6)
|
||||
assert.Equal(t, 5, list.size(), "three intervals grow forward")
|
||||
|
||||
list.MarkWritten(50, 65, 7)
|
||||
assert.Equal(t, 4, list.size(), "three intervals grow backward")
|
||||
|
||||
list.MarkWritten(70, 80, 8)
|
||||
assert.Equal(t, 5, list.size(), "four intervals")
|
||||
|
||||
list.MarkWritten(60, 70, 9)
|
||||
assert.Equal(t, 6, list.size(), "three intervals merged")
|
||||
|
||||
list.MarkWritten(59, 71, 10)
|
||||
assert.Equal(t, 6, list.size(), "covered three intervals")
|
||||
|
||||
list.MarkWritten(5, 59, 11)
|
||||
assert.Equal(t, 5, list.size(), "covered two intervals")
|
||||
|
||||
list.MarkWritten(70, 99, 12)
|
||||
assert.Equal(t, 5, list.size(), "covered one intervals")
|
||||
|
||||
}
|
||||
|
||||
type interval struct {
|
||||
start int64
|
||||
stop int64
|
||||
expected bool
|
||||
}
|
||||
|
||||
func Test_PageChunkWrittenIntervalList1(t *testing.T) {
|
||||
list := newChunkWrittenIntervalList()
|
||||
inputs := []interval{
|
||||
{1, 5, true},
|
||||
{2, 3, true},
|
||||
}
|
||||
for i, input := range inputs {
|
||||
list.MarkWritten(input.start, input.stop, int64(i)+1)
|
||||
actual := hasData(list, 0, 4)
|
||||
if actual != input.expected {
|
||||
t.Errorf("input [%d,%d) expected %v actual %v", input.start, input.stop, input.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasData(usage *ChunkWrittenIntervalList, chunkStartOffset, x int64) bool {
|
||||
for t := usage.head.next; t != usage.tail; t = t.next {
|
||||
logicStart := chunkStartOffset + t.StartOffset
|
||||
logicStop := chunkStartOffset + t.stopOffset
|
||||
if logicStart <= x && x < logicStop {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestIsContiguouslyWritten(t *testing.T) {
|
||||
const chunkSize int64 = 2 * 1024 * 1024
|
||||
cases := []struct {
|
||||
name string
|
||||
writes [][2]int64
|
||||
expected bool
|
||||
}{
|
||||
{name: "empty", writes: nil, expected: false},
|
||||
{name: "single full", writes: [][2]int64{{0, chunkSize}}, expected: true},
|
||||
{name: "single partial at start", writes: [][2]int64{{0, chunkSize / 4}}, expected: true},
|
||||
{name: "two adjacent halves in order", writes: [][2]int64{{0, chunkSize / 2}, {chunkSize / 2, chunkSize}}, expected: true},
|
||||
{name: "two adjacent halves out of order", writes: [][2]int64{{chunkSize / 2, chunkSize}, {0, chunkSize / 2}}, expected: true},
|
||||
{name: "overlapping covers everything", writes: [][2]int64{{0, chunkSize*3/4 + 1}, {chunkSize / 2, chunkSize}}, expected: true},
|
||||
{name: "three adjacent thirds", writes: [][2]int64{{0, chunkSize / 3}, {chunkSize / 3, 2 * chunkSize / 3}, {2 * chunkSize / 3, chunkSize}}, expected: true},
|
||||
{name: "single partial in middle", writes: [][2]int64{{chunkSize / 4, chunkSize / 2}}, expected: false},
|
||||
{name: "first byte missing", writes: [][2]int64{{1, chunkSize}}, expected: false},
|
||||
{name: "gap in middle", writes: [][2]int64{{0, chunkSize / 4}, {chunkSize / 2, chunkSize}}, expected: false},
|
||||
{name: "two intervals with 1-byte gap", writes: [][2]int64{{0, 100}, {101, 200}}, expected: false},
|
||||
{name: "three intervals with middle gap", writes: [][2]int64{{0, chunkSize / 4}, {chunkSize / 3, chunkSize / 2}, {chunkSize / 2, chunkSize}}, expected: false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
list := newChunkWrittenIntervalList()
|
||||
for i, w := range tc.writes {
|
||||
list.MarkWritten(w[0], w[1], int64(i+1))
|
||||
}
|
||||
assert.Equal(t, tc.expected, list.IsContiguouslyWritten(), "IsContiguouslyWritten mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsComplete_AdjacentIntervals(t *testing.T) {
|
||||
// Linux FUSE delivers writes up to FUSE_MAX_PAGES_PER_REQ
|
||||
// (typically 1 MiB) per op, so a 2 MiB chunk filled by sequential
|
||||
// writes arrives as two adjacent 1 MiB writes. addInterval does
|
||||
// not merge adjacent intervals into one, but IsComplete must
|
||||
// still report the chunk as fully covered — otherwise
|
||||
// maybeMoveToSealed never fires, the chunk stays writable
|
||||
// forever, and the write buffer cap cannot drain. Regression for
|
||||
// the TestWriteBufferCap Linux CI deadlock on PR #9066.
|
||||
const chunkSize int64 = 2 * 1024 * 1024
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
writes [][2]int64
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "single full write",
|
||||
writes: [][2]int64{{0, chunkSize}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "two adjacent halves in order",
|
||||
writes: [][2]int64{{0, chunkSize / 2}, {chunkSize / 2, chunkSize}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "two adjacent halves out of order",
|
||||
writes: [][2]int64{{chunkSize / 2, chunkSize}, {0, chunkSize / 2}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "eight adjacent eighths",
|
||||
writes: func() [][2]int64 {
|
||||
const n = 8
|
||||
step := chunkSize / n
|
||||
out := make([][2]int64, 0, n)
|
||||
for i := int64(0); i < n; i++ {
|
||||
out = append(out, [2]int64{i * step, (i + 1) * step})
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "gap in the middle",
|
||||
writes: [][2]int64{{0, chunkSize / 4}, {chunkSize / 2, chunkSize}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "missing last byte",
|
||||
writes: [][2]int64{{0, chunkSize - 1}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "missing first byte",
|
||||
writes: [][2]int64{{1, chunkSize}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "overlapping intervals covering everything",
|
||||
writes: [][2]int64{{0, chunkSize*3/4 + 1}, {chunkSize / 2, chunkSize}},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
list := newChunkWrittenIntervalList()
|
||||
for i, w := range tc.writes {
|
||||
list.MarkWritten(w[0], w[1], int64(i+1))
|
||||
}
|
||||
assert.Equal(t, tc.expected, list.IsComplete(chunkSize), "IsComplete mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
26
weed/mount/page_writer/dirty_pages.go
Normal file
26
weed/mount/page_writer/dirty_pages.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package page_writer
|
||||
|
||||
type DirtyPages interface {
|
||||
AddPage(offset int64, data []byte, isSequential bool, tsNs int64) error
|
||||
FlushData() error
|
||||
HasWrites() bool
|
||||
ReadDirtyDataAt(data []byte, startOffset int64, tsNs int64) (maxStop int64)
|
||||
Destroy()
|
||||
LockForRead(startOffset, stopOffset int64)
|
||||
UnlockForRead(startOffset, stopOffset int64)
|
||||
EvictOneWritableChunk() bool
|
||||
ProactiveFlush(nowNs, idleThresholdNs, maxHoldNs, fillRatio int64, frontierLag int, isSequential bool) bool
|
||||
}
|
||||
|
||||
func max(x, y int64) int64 {
|
||||
if x > y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
func min(x, y int64) int64 {
|
||||
if x < y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
19
weed/mount/page_writer/page_chunk.go
Normal file
19
weed/mount/page_writer/page_chunk.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type SaveToStorageFunc func(reader io.Reader, offset int64, size int64, modifiedTsNs int64, cleanupFn func())
|
||||
|
||||
type PageChunk interface {
|
||||
FreeResource()
|
||||
WriteDataAt(src []byte, offset int64, tsNs int64) (n int)
|
||||
ReadDataAt(p []byte, off int64, tsNs int64) (maxStop int64)
|
||||
IsComplete() bool
|
||||
IsContiguouslyWritten() bool
|
||||
ActivityScore() int64
|
||||
WrittenSize() int64
|
||||
LastWriteTsNs() int64
|
||||
SaveContent(saveFn SaveToStorageFunc)
|
||||
}
|
||||
127
weed/mount/page_writer/page_chunk_mem.go
Normal file
127
weed/mount/page_writer/page_chunk_mem.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/mem"
|
||||
)
|
||||
|
||||
var (
|
||||
_ = PageChunk(&MemChunk{})
|
||||
|
||||
memChunkCounter int64
|
||||
)
|
||||
|
||||
type MemChunk struct {
|
||||
sync.RWMutex
|
||||
buf []byte
|
||||
usage *ChunkWrittenIntervalList
|
||||
chunkSize int64
|
||||
logicChunkIndex LogicChunkIndex
|
||||
activityScore *ActivityScore
|
||||
lastWriteTsNs atomic.Int64
|
||||
}
|
||||
|
||||
func NewMemChunk(logicChunkIndex LogicChunkIndex, chunkSize int64) *MemChunk {
|
||||
atomic.AddInt64(&memChunkCounter, 1)
|
||||
return &MemChunk{
|
||||
logicChunkIndex: logicChunkIndex,
|
||||
chunkSize: chunkSize,
|
||||
buf: mem.Allocate(int(chunkSize)),
|
||||
usage: newChunkWrittenIntervalList(),
|
||||
activityScore: NewActivityScore(),
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MemChunk) FreeResource() {
|
||||
mc.Lock()
|
||||
defer mc.Unlock()
|
||||
|
||||
atomic.AddInt64(&memChunkCounter, -1)
|
||||
mem.Free(mc.buf)
|
||||
}
|
||||
|
||||
func (mc *MemChunk) WriteDataAt(src []byte, offset int64, tsNs int64) (n int) {
|
||||
mc.Lock()
|
||||
defer mc.Unlock()
|
||||
|
||||
innerOffset := offset % mc.chunkSize
|
||||
n = copy(mc.buf[innerOffset:], src)
|
||||
mc.usage.MarkWritten(innerOffset, innerOffset+int64(n), tsNs)
|
||||
mc.activityScore.MarkWrite()
|
||||
mc.lastWriteTsNs.Store(tsNs)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (mc *MemChunk) ReadDataAt(p []byte, off int64, tsNs int64) (maxStop int64) {
|
||||
mc.RLock()
|
||||
defer mc.RUnlock()
|
||||
|
||||
memChunkBaseOffset := int64(mc.logicChunkIndex) * mc.chunkSize
|
||||
for t := mc.usage.head.next; t != mc.usage.tail; t = t.next {
|
||||
logicStart := max(off, memChunkBaseOffset+t.StartOffset)
|
||||
logicStop := min(off+int64(len(p)), memChunkBaseOffset+t.stopOffset)
|
||||
if logicStart < logicStop {
|
||||
copy(p[logicStart-off:logicStop-off], mc.buf[logicStart-memChunkBaseOffset:logicStop-memChunkBaseOffset])
|
||||
maxStop = max(maxStop, logicStop)
|
||||
}
|
||||
}
|
||||
mc.activityScore.MarkRead()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (mc *MemChunk) IsComplete() bool {
|
||||
mc.RLock()
|
||||
defer mc.RUnlock()
|
||||
|
||||
return mc.usage.IsComplete(mc.chunkSize)
|
||||
}
|
||||
|
||||
func (mc *MemChunk) IsContiguouslyWritten() bool {
|
||||
mc.RLock()
|
||||
defer mc.RUnlock()
|
||||
|
||||
return mc.usage.IsContiguouslyWritten()
|
||||
}
|
||||
|
||||
func (mc *MemChunk) ActivityScore() int64 {
|
||||
return mc.activityScore.ActivityScore()
|
||||
}
|
||||
|
||||
func (mc *MemChunk) WrittenSize() int64 {
|
||||
mc.RLock()
|
||||
defer mc.RUnlock()
|
||||
|
||||
return mc.usage.WrittenSize()
|
||||
}
|
||||
|
||||
func (mc *MemChunk) LastWriteTsNs() int64 {
|
||||
return mc.lastWriteTsNs.Load()
|
||||
}
|
||||
|
||||
func (mc *MemChunk) SaveContent(saveFn SaveToStorageFunc) {
|
||||
mc.RLock()
|
||||
defer mc.RUnlock()
|
||||
|
||||
if saveFn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for t := mc.usage.head.next; t != mc.usage.tail; t = t.next {
|
||||
startOffset := t.StartOffset
|
||||
stopOffset := t.stopOffset
|
||||
tsNs := t.TsNs
|
||||
for t != mc.usage.tail && t.next.StartOffset == stopOffset {
|
||||
stopOffset = t.next.stopOffset
|
||||
t = t.next
|
||||
tsNs = max(tsNs, t.TsNs)
|
||||
}
|
||||
reader := util.NewBytesReader(mc.buf[startOffset:stopOffset])
|
||||
saveFn(reader, int64(mc.logicChunkIndex)*mc.chunkSize+startOffset, stopOffset-startOffset, tsNs, func() {
|
||||
})
|
||||
}
|
||||
}
|
||||
223
weed/mount/page_writer/page_chunk_swapfile.go
Normal file
223
weed/mount/page_writer/page_chunk_swapfile.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/mem"
|
||||
)
|
||||
|
||||
var (
|
||||
_ = PageChunk(&SwapFileChunk{})
|
||||
)
|
||||
|
||||
type ActualChunkIndex int
|
||||
|
||||
type SwapFile struct {
|
||||
dir string
|
||||
file *os.File
|
||||
chunkSize int64
|
||||
chunkTrackingLock sync.Mutex
|
||||
activeChunkCount int
|
||||
freeActualChunkList []ActualChunkIndex
|
||||
}
|
||||
|
||||
type SwapFileChunk struct {
|
||||
sync.RWMutex
|
||||
swapfile *SwapFile
|
||||
usage *ChunkWrittenIntervalList
|
||||
logicChunkIndex LogicChunkIndex
|
||||
actualChunkIndex ActualChunkIndex
|
||||
activityScore *ActivityScore
|
||||
lastWriteTsNs atomic.Int64
|
||||
//memChunk *MemChunk
|
||||
}
|
||||
|
||||
func NewSwapFile(dir string, chunkSize int64) *SwapFile {
|
||||
return &SwapFile{
|
||||
dir: dir,
|
||||
file: nil,
|
||||
chunkSize: chunkSize,
|
||||
}
|
||||
}
|
||||
func (sf *SwapFile) FreeResource() {
|
||||
sf.chunkTrackingLock.Lock()
|
||||
defer sf.chunkTrackingLock.Unlock()
|
||||
if sf.file != nil {
|
||||
sf.file.Close()
|
||||
os.Remove(sf.file.Name())
|
||||
sf.file = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sf *SwapFile) NewSwapFileChunk(logicChunkIndex LogicChunkIndex) (tc *SwapFileChunk) {
|
||||
sf.chunkTrackingLock.Lock()
|
||||
defer sf.chunkTrackingLock.Unlock()
|
||||
|
||||
if sf.file == nil {
|
||||
var err error
|
||||
sf.file, err = os.CreateTemp(sf.dir, "")
|
||||
if os.IsNotExist(err) {
|
||||
if mkdirErr := os.MkdirAll(sf.dir, 0700); mkdirErr != nil {
|
||||
glog.Errorf("create/recreate swap directory %s: %v", sf.dir, mkdirErr)
|
||||
return nil
|
||||
}
|
||||
sf.file, err = os.CreateTemp(sf.dir, "")
|
||||
}
|
||||
if err != nil {
|
||||
glog.Errorf("create swap file in %s: %v", sf.dir, err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
sf.activeChunkCount++
|
||||
|
||||
// assign a new physical chunk
|
||||
var actualChunkIndex ActualChunkIndex
|
||||
if len(sf.freeActualChunkList) > 0 {
|
||||
actualChunkIndex = sf.freeActualChunkList[0]
|
||||
sf.freeActualChunkList = sf.freeActualChunkList[1:]
|
||||
} else {
|
||||
actualChunkIndex = ActualChunkIndex(sf.activeChunkCount)
|
||||
}
|
||||
|
||||
swapFileChunk := &SwapFileChunk{
|
||||
swapfile: sf,
|
||||
usage: newChunkWrittenIntervalList(),
|
||||
logicChunkIndex: logicChunkIndex,
|
||||
actualChunkIndex: actualChunkIndex,
|
||||
activityScore: NewActivityScore(),
|
||||
// memChunk: NewMemChunk(logicChunkIndex, sf.chunkSize),
|
||||
}
|
||||
|
||||
// println(logicChunkIndex, "|", "++++", swapFileChunk.actualChunkIndex, swapFileChunk, sf)
|
||||
return swapFileChunk
|
||||
}
|
||||
|
||||
func (sc *SwapFileChunk) FreeResource() {
|
||||
|
||||
sc.Lock()
|
||||
defer sc.Unlock()
|
||||
|
||||
sc.swapfile.chunkTrackingLock.Lock()
|
||||
defer sc.swapfile.chunkTrackingLock.Unlock()
|
||||
|
||||
sc.swapfile.freeActualChunkList = append(sc.swapfile.freeActualChunkList, sc.actualChunkIndex)
|
||||
sc.swapfile.activeChunkCount--
|
||||
// println(sc.logicChunkIndex, "|", "----", sc.actualChunkIndex, sc, sc.swapfile)
|
||||
}
|
||||
|
||||
func (sc *SwapFileChunk) WriteDataAt(src []byte, offset int64, tsNs int64) (n int) {
|
||||
sc.Lock()
|
||||
defer sc.Unlock()
|
||||
|
||||
// println(sc.logicChunkIndex, "|", tsNs, "write at", offset, len(src), sc.actualChunkIndex)
|
||||
|
||||
innerOffset := offset % sc.swapfile.chunkSize
|
||||
var err error
|
||||
n, err = sc.swapfile.file.WriteAt(src, int64(sc.actualChunkIndex)*sc.swapfile.chunkSize+innerOffset)
|
||||
sc.usage.MarkWritten(innerOffset, innerOffset+int64(n), tsNs)
|
||||
if err != nil {
|
||||
glog.Errorf("failed to write swap file %s: %v", sc.swapfile.file.Name(), err)
|
||||
}
|
||||
//sc.memChunk.WriteDataAt(src, offset, tsNs)
|
||||
sc.activityScore.MarkWrite()
|
||||
sc.lastWriteTsNs.Store(tsNs)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (sc *SwapFileChunk) ReadDataAt(p []byte, off int64, tsNs int64) (maxStop int64) {
|
||||
sc.RLock()
|
||||
defer sc.RUnlock()
|
||||
|
||||
// println(sc.logicChunkIndex, "|", tsNs, "read at", off, len(p), sc.actualChunkIndex)
|
||||
|
||||
//memCopy := make([]byte, len(p))
|
||||
//copy(memCopy, p)
|
||||
|
||||
chunkStartOffset := int64(sc.logicChunkIndex) * sc.swapfile.chunkSize
|
||||
for t := sc.usage.head.next; t != sc.usage.tail; t = t.next {
|
||||
logicStart := max(off, chunkStartOffset+t.StartOffset)
|
||||
logicStop := min(off+int64(len(p)), chunkStartOffset+t.stopOffset)
|
||||
if logicStart < logicStop {
|
||||
actualStart := logicStart - chunkStartOffset + int64(sc.actualChunkIndex)*sc.swapfile.chunkSize
|
||||
if n, err := sc.swapfile.file.ReadAt(p[logicStart-off:logicStop-off], actualStart); err != nil {
|
||||
if err == io.EOF && n == int(logicStop-logicStart) {
|
||||
err = nil
|
||||
}
|
||||
glog.Errorf("failed to reading swap file %s: %v", sc.swapfile.file.Name(), err)
|
||||
break
|
||||
}
|
||||
maxStop = max(maxStop, logicStop)
|
||||
}
|
||||
}
|
||||
//sc.memChunk.ReadDataAt(memCopy, off, tsNs)
|
||||
//if bytes.Compare(memCopy, p) != 0 {
|
||||
// println("read wrong data from swap file", off, sc.logicChunkIndex)
|
||||
//}
|
||||
|
||||
sc.activityScore.MarkRead()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (sc *SwapFileChunk) IsComplete() bool {
|
||||
sc.RLock()
|
||||
defer sc.RUnlock()
|
||||
return sc.usage.IsComplete(sc.swapfile.chunkSize)
|
||||
}
|
||||
|
||||
func (sc *SwapFileChunk) IsContiguouslyWritten() bool {
|
||||
sc.RLock()
|
||||
defer sc.RUnlock()
|
||||
return sc.usage.IsContiguouslyWritten()
|
||||
}
|
||||
|
||||
func (sc *SwapFileChunk) ActivityScore() int64 {
|
||||
return sc.activityScore.ActivityScore()
|
||||
}
|
||||
|
||||
func (sc *SwapFileChunk) WrittenSize() int64 {
|
||||
sc.RLock()
|
||||
defer sc.RUnlock()
|
||||
return sc.usage.WrittenSize()
|
||||
}
|
||||
|
||||
func (sc *SwapFileChunk) LastWriteTsNs() int64 {
|
||||
return sc.lastWriteTsNs.Load()
|
||||
}
|
||||
|
||||
func (sc *SwapFileChunk) SaveContent(saveFn SaveToStorageFunc) {
|
||||
sc.RLock()
|
||||
defer sc.RUnlock()
|
||||
|
||||
if saveFn == nil {
|
||||
return
|
||||
}
|
||||
// println(sc.logicChunkIndex, "|", "save")
|
||||
for t := sc.usage.head.next; t != sc.usage.tail; t = t.next {
|
||||
startOffset := t.StartOffset
|
||||
stopOffset := t.stopOffset
|
||||
tsNs := t.TsNs
|
||||
for t != sc.usage.tail && t.next.StartOffset == stopOffset {
|
||||
stopOffset = t.next.stopOffset
|
||||
t = t.next
|
||||
tsNs = max(tsNs, t.TsNs)
|
||||
}
|
||||
|
||||
data := mem.Allocate(int(stopOffset - startOffset))
|
||||
n, _ := sc.swapfile.file.ReadAt(data, startOffset+int64(sc.actualChunkIndex)*sc.swapfile.chunkSize)
|
||||
if n > 0 {
|
||||
reader := util.NewBytesReader(data[:n])
|
||||
saveFn(reader, int64(sc.logicChunkIndex)*sc.swapfile.chunkSize+startOffset, int64(n), tsNs, func() {
|
||||
})
|
||||
}
|
||||
mem.Free(data)
|
||||
}
|
||||
|
||||
}
|
||||
105
weed/mount/page_writer/page_chunk_swapfile_test.go
Normal file
105
weed/mount/page_writer/page_chunk_swapfile_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSwapFile_NewSwapFileChunk_Concurrent(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "seaweedfs_swap_test")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
sf := NewSwapFile(filepath.Join(tempDir, "swap"), 1024)
|
||||
defer sf.FreeResource()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var failures atomic.Uint32
|
||||
numConcurrent := 10
|
||||
for i := 0; i < numConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
chunk := sf.NewSwapFileChunk(LogicChunkIndex(idx))
|
||||
if chunk == nil {
|
||||
failures.Add(1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if failures.Load() > 0 {
|
||||
t.Errorf("failed to create %d chunks", failures.Load())
|
||||
}
|
||||
|
||||
if sf.file == nil {
|
||||
t.Error("sf.file should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwapFile_MkdirAll_Permissions(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "seaweedfs_swap_perm_test")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
swapDir := filepath.Join(tempDir, "swap_subdir")
|
||||
sf := NewSwapFile(swapDir, 1024)
|
||||
defer sf.FreeResource()
|
||||
|
||||
// This should trigger os.MkdirAll
|
||||
if sf.NewSwapFileChunk(0) == nil {
|
||||
t.Fatal("NewSwapFileChunk failed to create a chunk")
|
||||
}
|
||||
|
||||
info, err := os.Stat(swapDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat swap dir: %v", err)
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
t.Errorf("expected %s to be a directory", swapDir)
|
||||
}
|
||||
|
||||
// Check permissions - should be 0700
|
||||
if info.Mode().Perm() != 0700 {
|
||||
t.Errorf("expected permissions 0700, got %o", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwapFile_RecreateDir(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "seaweedfs_swap_recreate_test")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
swapDir := filepath.Join(tempDir, "swap_recreate")
|
||||
sf := NewSwapFile(swapDir, 1024)
|
||||
|
||||
// Create first chunk
|
||||
if sf.NewSwapFileChunk(0) == nil {
|
||||
t.Fatal("NewSwapFileChunk failed to create the first chunk")
|
||||
}
|
||||
sf.FreeResource()
|
||||
|
||||
// Delete the directory
|
||||
os.RemoveAll(swapDir)
|
||||
|
||||
// Create second chunk - should recreate directory
|
||||
chunk := sf.NewSwapFileChunk(1)
|
||||
if chunk == nil {
|
||||
t.Error("failed to create chunk after directory deletion")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(swapDir); os.IsNotExist(err) {
|
||||
t.Error("swap directory was not recreated")
|
||||
}
|
||||
sf.FreeResource()
|
||||
}
|
||||
402
weed/mount/page_writer/upload_pipeline.go
Normal file
402
weed/mount/page_writer/upload_pipeline.go
Normal file
@@ -0,0 +1,402 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
type LogicChunkIndex int
|
||||
|
||||
type UploadPipeline struct {
|
||||
uploaderCount int32
|
||||
uploaderCountCond *sync.Cond
|
||||
filepath util.FullPath
|
||||
ChunkSize int64
|
||||
uploaders *util.LimitedConcurrentExecutor
|
||||
saveToStorageFn SaveToStorageFunc
|
||||
writableChunkLimit int
|
||||
concurrentWriterMax int32
|
||||
swapFile *SwapFile
|
||||
chunksLock sync.Mutex
|
||||
writableChunks map[LogicChunkIndex]PageChunk
|
||||
sealedChunks map[LogicChunkIndex]*SealedChunk
|
||||
activeReadChunks map[LogicChunkIndex]int
|
||||
readerCountCond *sync.Cond
|
||||
accountant *WriteBufferAccountant
|
||||
lastWriteChunkIndex int64 // atomic: highest LogicChunkIndex written
|
||||
}
|
||||
|
||||
type SealedChunk struct {
|
||||
chunk PageChunk
|
||||
referenceCounter int // track uploading or reading processes
|
||||
// accountant and chunkSize are captured when the chunk is sealed so
|
||||
// FreeReference can release the global write-budget slot exactly once
|
||||
// regardless of which code path triggers the final deref (normal
|
||||
// upload completion, Shutdown, or replace-in-place in moveToSealed).
|
||||
accountant *WriteBufferAccountant
|
||||
chunkSize int64
|
||||
}
|
||||
|
||||
func (sc *SealedChunk) FreeReference(messageOnFree string) {
|
||||
// Early-return guard so repeated calls (Shutdown racing the async
|
||||
// uploader, or any future caller that loses track of ownership) are
|
||||
// strict no-ops rather than driving referenceCounter negative.
|
||||
if sc.referenceCounter <= 0 {
|
||||
return
|
||||
}
|
||||
sc.referenceCounter--
|
||||
if sc.referenceCounter == 0 {
|
||||
glog.V(4).Infof("Free sealed chunk: %s", messageOnFree)
|
||||
sc.chunk.FreeResource()
|
||||
sc.accountant.Release(sc.chunkSize)
|
||||
}
|
||||
}
|
||||
|
||||
// NewUploadPipeline constructs an UploadPipeline. accountant may be nil,
|
||||
// in which case no global write-buffer cap is enforced. When non-nil,
|
||||
// creating a new page chunk (memory or swap) first reserves ChunkSize
|
||||
// bytes against it, blocking the writer if the global cap is reached.
|
||||
// The accountant is captured at construction so the pipeline's hot paths
|
||||
// (SaveDataAt, moveToSealed, Shutdown) can read up.accountant without
|
||||
// any synchronization.
|
||||
func NewUploadPipeline(writers *util.LimitedConcurrentExecutor, chunkSize int64, saveToStorageFn SaveToStorageFunc, bufferChunkLimit int, swapFileDir string, accountant *WriteBufferAccountant) *UploadPipeline {
|
||||
t := &UploadPipeline{
|
||||
ChunkSize: chunkSize,
|
||||
writableChunks: make(map[LogicChunkIndex]PageChunk),
|
||||
sealedChunks: make(map[LogicChunkIndex]*SealedChunk),
|
||||
uploaders: writers,
|
||||
uploaderCountCond: sync.NewCond(&sync.Mutex{}),
|
||||
saveToStorageFn: saveToStorageFn,
|
||||
activeReadChunks: make(map[LogicChunkIndex]int),
|
||||
writableChunkLimit: bufferChunkLimit,
|
||||
concurrentWriterMax: int32(bufferChunkLimit),
|
||||
swapFile: NewSwapFile(swapFileDir, chunkSize),
|
||||
accountant: accountant,
|
||||
}
|
||||
t.readerCountCond = sync.NewCond(&t.chunksLock)
|
||||
return t
|
||||
}
|
||||
|
||||
func (up *UploadPipeline) SaveDataAt(p []byte, off int64, isSequential bool, tsNs int64) (n int, err error) {
|
||||
|
||||
up.chunksLock.Lock()
|
||||
defer up.chunksLock.Unlock()
|
||||
|
||||
logicChunkIndex := LogicChunkIndex(off / up.ChunkSize)
|
||||
|
||||
// track write frontier for proactive flushing (CAS to avoid regression)
|
||||
for {
|
||||
old := atomic.LoadInt64(&up.lastWriteChunkIndex)
|
||||
if int64(logicChunkIndex) <= old {
|
||||
break
|
||||
}
|
||||
if atomic.CompareAndSwapInt64(&up.lastWriteChunkIndex, old, int64(logicChunkIndex)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pageChunk, found := up.writableChunks[logicChunkIndex]
|
||||
if !found {
|
||||
// Reserve a chunk-sized slot against the global write budget before
|
||||
// allocating. Reserve may block when volumes are full and sealed
|
||||
// chunks can't drain, so we must release chunksLock first — the
|
||||
// uploader goroutines that eventually call Release take chunksLock.
|
||||
if up.accountant != nil {
|
||||
up.chunksLock.Unlock()
|
||||
up.accountant.Reserve(up.ChunkSize)
|
||||
up.chunksLock.Lock()
|
||||
// Re-check: another writer on the same file may have created
|
||||
// the chunk while we were blocked. If so, give the slot back.
|
||||
if pageChunk, found = up.writableChunks[logicChunkIndex]; found {
|
||||
up.accountant.Release(up.ChunkSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if len(up.writableChunks) > up.writableChunkLimit {
|
||||
// Per-pipeline soft cap. Seal the fullest gap-free chunk;
|
||||
// gappy chunks must wait so in-flight FUSE writeback can fill
|
||||
// the gap (issue #9330). If none qualifies, fall through —
|
||||
// the memChunkCounter ceiling below redirects to swap.
|
||||
candidateChunkIndex, fullness := LogicChunkIndex(-1), int64(0)
|
||||
for lci, mc := range up.writableChunks {
|
||||
if !mc.IsContiguouslyWritten() {
|
||||
continue
|
||||
}
|
||||
if b := mc.WrittenSize(); b > fullness {
|
||||
candidateChunkIndex = lci
|
||||
fullness = b
|
||||
}
|
||||
}
|
||||
if candidateChunkIndex >= 0 {
|
||||
up.moveToSealed(up.writableChunks[candidateChunkIndex], candidateChunkIndex)
|
||||
}
|
||||
}
|
||||
// fmt.Printf("isSequential:%v len(up.writableChunks):%v memChunkCounter:%v", isSequential, len(up.writableChunks), memChunkCounter)
|
||||
if isSequential &&
|
||||
len(up.writableChunks) < up.writableChunkLimit &&
|
||||
atomic.LoadInt64(&memChunkCounter) < 4*int64(up.writableChunkLimit) {
|
||||
pageChunk = NewMemChunk(logicChunkIndex, up.ChunkSize)
|
||||
// fmt.Printf(" create mem chunk %d\n", logicChunkIndex)
|
||||
} else {
|
||||
pageChunk = up.swapFile.NewSwapFileChunk(logicChunkIndex)
|
||||
// fmt.Printf(" create file chunk %d\n", logicChunkIndex)
|
||||
if pageChunk == nil {
|
||||
up.accountant.Release(up.ChunkSize)
|
||||
return 0, fmt.Errorf("failed to create swap file chunk")
|
||||
}
|
||||
}
|
||||
up.writableChunks[logicChunkIndex] = pageChunk
|
||||
}
|
||||
//if _, foundSealed := up.sealedChunks[logicChunkIndex]; foundSealed {
|
||||
// println("found already sealed chunk", logicChunkIndex)
|
||||
//}
|
||||
//if _, foundReading := up.activeReadChunks[logicChunkIndex]; foundReading {
|
||||
// println("found active read chunk", logicChunkIndex)
|
||||
//}
|
||||
n = pageChunk.WriteDataAt(p, off, tsNs)
|
||||
up.maybeMoveToSealed(pageChunk, logicChunkIndex)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (up *UploadPipeline) MaybeReadDataAt(p []byte, off int64, tsNs int64) (maxStop int64) {
|
||||
logicChunkIndex := LogicChunkIndex(off / up.ChunkSize)
|
||||
|
||||
up.chunksLock.Lock()
|
||||
defer func() {
|
||||
up.readerCountCond.Signal()
|
||||
up.chunksLock.Unlock()
|
||||
}()
|
||||
|
||||
// read from sealed chunks first
|
||||
sealedChunk, found := up.sealedChunks[logicChunkIndex]
|
||||
if found {
|
||||
maxStop = sealedChunk.chunk.ReadDataAt(p, off, tsNs)
|
||||
glog.V(4).Infof("%s read sealed memchunk [%d,%d)", up.filepath, off, maxStop)
|
||||
}
|
||||
|
||||
// read from writable chunks last
|
||||
writableChunk, found := up.writableChunks[logicChunkIndex]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
writableMaxStop := writableChunk.ReadDataAt(p, off, tsNs)
|
||||
glog.V(4).Infof("%s read writable memchunk [%d,%d)", up.filepath, off, writableMaxStop)
|
||||
maxStop = max(maxStop, writableMaxStop)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (up *UploadPipeline) FlushAll() {
|
||||
up.flushChunks()
|
||||
up.waitForCurrentWritersToComplete()
|
||||
}
|
||||
|
||||
func (up *UploadPipeline) flushChunks() {
|
||||
up.chunksLock.Lock()
|
||||
defer up.chunksLock.Unlock()
|
||||
|
||||
for logicChunkIndex, memChunk := range up.writableChunks {
|
||||
up.moveToSealed(memChunk, logicChunkIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func (up *UploadPipeline) maybeMoveToSealed(memChunk PageChunk, logicChunkIndex LogicChunkIndex) {
|
||||
if memChunk.IsComplete() {
|
||||
up.moveToSealed(memChunk, logicChunkIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func (up *UploadPipeline) moveToSealed(memChunk PageChunk, logicChunkIndex LogicChunkIndex) {
|
||||
newCount := atomic.AddInt32(&up.uploaderCount, 1)
|
||||
glog.V(4).Infof("%s uploaderCount %d ++> %d", up.filepath, newCount-1, newCount)
|
||||
|
||||
if oldMemChunk, found := up.sealedChunks[logicChunkIndex]; found {
|
||||
oldMemChunk.FreeReference(fmt.Sprintf("%s replace chunk %d", up.filepath, logicChunkIndex))
|
||||
}
|
||||
sealedChunk := &SealedChunk{
|
||||
chunk: memChunk,
|
||||
// One for the sealedChunks slot, one for the upload below, which
|
||||
// drops its own. Execute() returns before SaveContent reads the
|
||||
// chunk, so a later seal must not free the buffer under it.
|
||||
referenceCounter: 2,
|
||||
accountant: up.accountant,
|
||||
chunkSize: up.ChunkSize,
|
||||
}
|
||||
up.sealedChunks[logicChunkIndex] = sealedChunk
|
||||
delete(up.writableChunks, logicChunkIndex)
|
||||
|
||||
// unlock before submitting the uploading jobs
|
||||
up.chunksLock.Unlock()
|
||||
up.uploaders.Execute(func() {
|
||||
// first add to the file chunks
|
||||
sealedChunk.chunk.SaveContent(up.saveToStorageFn)
|
||||
|
||||
// notify waiting process
|
||||
newCount := atomic.AddInt32(&up.uploaderCount, -1)
|
||||
glog.V(4).Infof("%s uploaderCount %d --> %d", up.filepath, newCount+1, newCount)
|
||||
// Lock and Unlock are not required,
|
||||
// but it may signal multiple times during one wakeup,
|
||||
// and the waiting goroutine may miss some of them!
|
||||
up.uploaderCountCond.L.Lock()
|
||||
up.uploaderCountCond.Broadcast()
|
||||
up.uploaderCountCond.L.Unlock()
|
||||
|
||||
// wait for readers
|
||||
up.chunksLock.Lock()
|
||||
defer up.chunksLock.Unlock()
|
||||
for up.IsLocked(logicChunkIndex) {
|
||||
up.readerCountCond.Wait()
|
||||
}
|
||||
|
||||
// then remove from sealed chunks, unless a later seal took over the
|
||||
// index — deleting that one would hide its dirty pages from readers
|
||||
if up.sealedChunks[logicChunkIndex] == sealedChunk {
|
||||
delete(up.sealedChunks, logicChunkIndex)
|
||||
sealedChunk.FreeReference(fmt.Sprintf("%s unindex chunk %d", up.filepath, logicChunkIndex))
|
||||
}
|
||||
sealedChunk.FreeReference(fmt.Sprintf("%s finished uploading chunk %d", up.filepath, logicChunkIndex))
|
||||
|
||||
})
|
||||
up.chunksLock.Lock()
|
||||
}
|
||||
|
||||
// EvictOneWritableChunk force-seals one writable chunk so the accountant's
|
||||
// Reserve loop can make progress. Strict pass picks the fullest gap-free
|
||||
// chunk (issue #9330). Fallback picks the oldest non-empty writer when
|
||||
// nothing is gap-free; without it Reserve deadlocks (cond.Wait only wakes
|
||||
// on Release, Release only fires on upload completion). Callers must not
|
||||
// hold up.chunksLock.
|
||||
func (up *UploadPipeline) EvictOneWritableChunk() bool {
|
||||
up.chunksLock.Lock()
|
||||
defer up.chunksLock.Unlock()
|
||||
if len(up.writableChunks) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
bestIndex := LogicChunkIndex(-1)
|
||||
var bestBytes int64
|
||||
for lci, wc := range up.writableChunks {
|
||||
if !wc.IsContiguouslyWritten() {
|
||||
continue
|
||||
}
|
||||
if b := wc.WrittenSize(); b > bestBytes {
|
||||
bestIndex = lci
|
||||
bestBytes = b
|
||||
}
|
||||
}
|
||||
|
||||
if bestIndex < 0 {
|
||||
oldestTsNs := int64(math.MaxInt64)
|
||||
var fallbackBytes int64
|
||||
for lci, wc := range up.writableChunks {
|
||||
b := wc.WrittenSize()
|
||||
if b == 0 {
|
||||
continue
|
||||
}
|
||||
ts := wc.LastWriteTsNs()
|
||||
if ts == 0 {
|
||||
continue
|
||||
}
|
||||
if ts < oldestTsNs || (ts == oldestTsNs && b > fallbackBytes) {
|
||||
bestIndex = lci
|
||||
oldestTsNs = ts
|
||||
fallbackBytes = b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bestIndex < 0 {
|
||||
return false
|
||||
}
|
||||
up.moveToSealed(up.writableChunks[bestIndex], bestIndex)
|
||||
return true
|
||||
}
|
||||
|
||||
// ProactiveFlush seals at most one idle writable chunk that is unlikely to
|
||||
// receive further writes, submitting it for async upload. Returns true if a
|
||||
// chunk was sealed. The caller (ChunkFlusher) invokes this periodically so
|
||||
// that partially-written chunks drain continuously instead of piling up
|
||||
// until fsync.
|
||||
func (up *UploadPipeline) ProactiveFlush(nowNs int64, idleThresholdNs int64, maxHoldNs int64, fillRatio int64, frontierLag int, isSequential bool) bool {
|
||||
if up.concurrentWriterMax <= 0 || atomic.LoadInt32(&up.uploaderCount)*2 >= up.concurrentWriterMax {
|
||||
return false
|
||||
}
|
||||
|
||||
up.chunksLock.Lock()
|
||||
defer up.chunksLock.Unlock()
|
||||
|
||||
if len(up.writableChunks) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
frontier := atomic.LoadInt64(&up.lastWriteChunkIndex)
|
||||
isSeq := isSequential
|
||||
|
||||
var bestIdx LogicChunkIndex = -1
|
||||
var bestBytes int64 = -1
|
||||
|
||||
for lci, chunk := range up.writableChunks {
|
||||
lastWrite := chunk.LastWriteTsNs()
|
||||
if lastWrite == 0 {
|
||||
continue
|
||||
}
|
||||
age := nowNs - lastWrite
|
||||
if age < idleThresholdNs {
|
||||
continue
|
||||
}
|
||||
// Skip gappy chunks; FUSE writeback may still be in flight for the
|
||||
// hole and SaveContent would bake it into split volume chunks
|
||||
// (issue #9330). Failing here is just a missed flush — FlushAll
|
||||
// at close still seals everything.
|
||||
if !chunk.IsContiguouslyWritten() {
|
||||
continue
|
||||
}
|
||||
written := chunk.WrittenSize()
|
||||
nearlyFull := written >= fillRatio
|
||||
behindFrontier := isSeq && int64(lci) <= frontier-int64(frontierLag)
|
||||
stale := age >= maxHoldNs
|
||||
|
||||
if !nearlyFull && !behindFrontier && !stale {
|
||||
continue
|
||||
}
|
||||
if written > bestBytes {
|
||||
bestIdx = lci
|
||||
bestBytes = written
|
||||
}
|
||||
}
|
||||
if bestIdx < 0 {
|
||||
return false
|
||||
}
|
||||
glog.V(3).Infof("%s proactive flush chunk %d (%d bytes written)", up.filepath, bestIdx, bestBytes)
|
||||
up.moveToSealed(up.writableChunks[bestIdx], bestIdx)
|
||||
return true
|
||||
}
|
||||
|
||||
func (up *UploadPipeline) Shutdown() {
|
||||
up.swapFile.FreeResource()
|
||||
|
||||
up.chunksLock.Lock()
|
||||
defer up.chunksLock.Unlock()
|
||||
// Free any writable chunks that were reserved but never sealed — on the
|
||||
// Destroy() path (truncate / metadata invalidation) there is no
|
||||
// preceding FlushData(), so dirty writable chunks would otherwise leak
|
||||
// both their memory and their write-budget slots.
|
||||
for logicChunkIndex, writableChunk := range up.writableChunks {
|
||||
glog.V(4).Infof("%s uploadpipeline shutdown writable chunk %d", up.filepath, logicChunkIndex)
|
||||
writableChunk.FreeResource()
|
||||
up.accountant.Release(up.ChunkSize)
|
||||
delete(up.writableChunks, logicChunkIndex)
|
||||
}
|
||||
for logicChunkIndex, sealedChunk := range up.sealedChunks {
|
||||
// only the slot reference; an in-flight upload frees the chunk itself
|
||||
sealedChunk.FreeReference(fmt.Sprintf("%s uploadpipeline shutdown chunk %d", up.filepath, logicChunkIndex))
|
||||
delete(up.sealedChunks, logicChunkIndex)
|
||||
}
|
||||
}
|
||||
61
weed/mount/page_writer/upload_pipeline_lock.go
Normal file
61
weed/mount/page_writer/upload_pipeline_lock.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
func (up *UploadPipeline) LockForRead(startOffset, stopOffset int64) {
|
||||
startLogicChunkIndex := LogicChunkIndex(startOffset / up.ChunkSize)
|
||||
stopLogicChunkIndex := LogicChunkIndex(stopOffset / up.ChunkSize)
|
||||
if stopOffset%up.ChunkSize > 0 {
|
||||
stopLogicChunkIndex += 1
|
||||
}
|
||||
up.chunksLock.Lock()
|
||||
defer up.chunksLock.Unlock()
|
||||
for i := startLogicChunkIndex; i < stopLogicChunkIndex; i++ {
|
||||
if count, found := up.activeReadChunks[i]; found {
|
||||
up.activeReadChunks[i] = count + 1
|
||||
} else {
|
||||
up.activeReadChunks[i] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (up *UploadPipeline) UnlockForRead(startOffset, stopOffset int64) {
|
||||
startLogicChunkIndex := LogicChunkIndex(startOffset / up.ChunkSize)
|
||||
stopLogicChunkIndex := LogicChunkIndex(stopOffset / up.ChunkSize)
|
||||
if stopOffset%up.ChunkSize > 0 {
|
||||
stopLogicChunkIndex += 1
|
||||
}
|
||||
up.chunksLock.Lock()
|
||||
defer up.chunksLock.Unlock()
|
||||
for i := startLogicChunkIndex; i < stopLogicChunkIndex; i++ {
|
||||
if count, found := up.activeReadChunks[i]; found {
|
||||
if count == 1 {
|
||||
delete(up.activeReadChunks, i)
|
||||
} else {
|
||||
up.activeReadChunks[i] = count - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (up *UploadPipeline) IsLocked(logicChunkIndex LogicChunkIndex) bool {
|
||||
if count, found := up.activeReadChunks[logicChunkIndex]; found {
|
||||
return count > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (up *UploadPipeline) waitForCurrentWritersToComplete() {
|
||||
up.uploaderCountCond.L.Lock()
|
||||
t := int32(100)
|
||||
for {
|
||||
t = atomic.LoadInt32(&up.uploaderCount)
|
||||
if t <= 0 {
|
||||
break
|
||||
}
|
||||
up.uploaderCountCond.Wait()
|
||||
}
|
||||
up.uploaderCountCond.L.Unlock()
|
||||
}
|
||||
121
weed/mount/page_writer/upload_pipeline_reseal_test.go
Normal file
121
weed/mount/page_writer/upload_pipeline_reseal_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// gatedChunk parks in SaveContent until the test opens its gate: the state
|
||||
// moveToSealed leaves an upload in, submitted but not yet reading its buffer.
|
||||
type gatedChunk struct {
|
||||
gate chan struct{}
|
||||
saved chan struct{}
|
||||
freedBefore bool // FreeResource had already run when SaveContent began
|
||||
freeCount atomic.Int32
|
||||
}
|
||||
|
||||
func newGatedChunk() *gatedChunk {
|
||||
return &gatedChunk{
|
||||
gate: make(chan struct{}),
|
||||
saved: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *gatedChunk) FreeResource() { c.freeCount.Add(1) }
|
||||
func (c *gatedChunk) WriteDataAt([]byte, int64, int64) int { return 0 }
|
||||
func (c *gatedChunk) ReadDataAt([]byte, int64, int64) int64 { return 0 }
|
||||
func (c *gatedChunk) IsComplete() bool { return false }
|
||||
func (c *gatedChunk) IsContiguouslyWritten() bool { return true }
|
||||
func (c *gatedChunk) ActivityScore() int64 { return 0 }
|
||||
func (c *gatedChunk) WrittenSize() int64 { return 0 }
|
||||
func (c *gatedChunk) LastWriteTsNs() int64 { return 0 }
|
||||
func (c *gatedChunk) SaveContent(saveFn SaveToStorageFunc) {
|
||||
<-c.gate
|
||||
c.freedBefore = c.freeCount.Load() > 0
|
||||
close(c.saved)
|
||||
}
|
||||
|
||||
// Sealing the same index twice must not free the first chunk: freeing it
|
||||
// returns a live buffer to the slot pool and the in-flight upload then ships
|
||||
// whatever the next NewMemChunk writes there.
|
||||
func TestMoveToSealed_ReplaceKeepsUploadReference(t *testing.T) {
|
||||
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(2), 2*1024*1024, nil, 16, "", nil)
|
||||
|
||||
first, second := newGatedChunk(), newGatedChunk()
|
||||
|
||||
up.chunksLock.Lock()
|
||||
up.moveToSealed(first, 0)
|
||||
up.moveToSealed(second, 0) // takes over index 0 while first is uploading
|
||||
up.chunksLock.Unlock()
|
||||
|
||||
// both seals are done, so any premature free has already happened
|
||||
close(first.gate)
|
||||
select {
|
||||
case <-first.saved:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("first chunk's SaveContent never ran")
|
||||
}
|
||||
if first.freedBefore {
|
||||
t.Error("first chunk was freed before its upload read it")
|
||||
}
|
||||
|
||||
// a finished upload must leave the newer chunk indexed for readers
|
||||
waitForFree(t, first)
|
||||
up.chunksLock.Lock()
|
||||
indexed := up.sealedChunks[0]
|
||||
up.chunksLock.Unlock()
|
||||
if indexed == nil || indexed.chunk != second {
|
||||
t.Errorf("index 0 should still hold the second chunk, got %v", indexed)
|
||||
}
|
||||
|
||||
close(second.gate)
|
||||
waitForFree(t, second)
|
||||
if got := first.freeCount.Load(); got != 1 {
|
||||
t.Errorf("first chunk freed %d times, want 1", got)
|
||||
}
|
||||
if got := second.freeCount.Load(); got != 1 {
|
||||
t.Errorf("second chunk freed %d times, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown drops only the slot reference; the in-flight upload keeps the
|
||||
// chunk alive until it is done, then frees it exactly once.
|
||||
func TestShutdown_LeavesInFlightUploadItsReference(t *testing.T) {
|
||||
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(2), 2*1024*1024, nil, 16, t.TempDir(), nil)
|
||||
|
||||
chunk := newGatedChunk()
|
||||
up.chunksLock.Lock()
|
||||
up.moveToSealed(chunk, 0)
|
||||
up.chunksLock.Unlock()
|
||||
|
||||
up.Shutdown()
|
||||
|
||||
close(chunk.gate)
|
||||
select {
|
||||
case <-chunk.saved:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("SaveContent never ran")
|
||||
}
|
||||
if chunk.freedBefore {
|
||||
t.Error("Shutdown freed the chunk before its upload read it")
|
||||
}
|
||||
waitForFree(t, chunk)
|
||||
if got := chunk.freeCount.Load(); got != 1 {
|
||||
t.Errorf("chunk freed %d times, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForFree(t *testing.T, c *gatedChunk) {
|
||||
t.Helper()
|
||||
<-c.saved
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for c.freeCount.Load() == 0 {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("chunk was never freed after its upload finished")
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
61
weed/mount/page_writer/upload_pipeline_shutdown_test.go
Normal file
61
weed/mount/page_writer/upload_pipeline_shutdown_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestShutdown_ReleasesWritableReservation verifies that calling Shutdown
|
||||
// on a pipeline with dirty-but-unsealed writable chunks returns those
|
||||
// chunks' reservations to the accountant. Regression for the Destroy()
|
||||
// path (truncate / metadata invalidation) which never flushes before
|
||||
// dropping the pipeline, so writable chunks would otherwise leak their
|
||||
// write-budget slots permanently.
|
||||
func TestShutdown_ReleasesWritableReservation(t *testing.T) {
|
||||
acc := NewWriteBufferAccountant(1024 * 1024)
|
||||
saveFn := func(io.Reader, int64, int64, int64, func()) {}
|
||||
up := NewUploadPipeline(nil, 64*1024, saveFn, 16, t.TempDir(), acc)
|
||||
|
||||
// A small partial write lands in writableChunks as a mem chunk and
|
||||
// stays there (the chunk is not complete so maybeMoveToSealed is a
|
||||
// no-op) — exactly the state Destroy() can hit.
|
||||
buf := make([]byte, 128)
|
||||
if _, err := up.SaveDataAt(buf, 0, true, 1); err != nil {
|
||||
t.Fatalf("SaveDataAt: %v", err)
|
||||
}
|
||||
if got := acc.Used(); got != 64*1024 {
|
||||
t.Fatalf("expected 64KiB reserved after one write, got %d", got)
|
||||
}
|
||||
|
||||
up.Shutdown()
|
||||
|
||||
if got := acc.Used(); got != 0 {
|
||||
t.Fatalf("expected 0 used after Shutdown, got %d (writable reservation leaked)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSealedChunk_FreeReferenceIsIdempotent verifies that FreeReference
|
||||
// releases the accountant slot exactly once even if it is invoked more
|
||||
// than once. This guards the path where Shutdown and the async uploader
|
||||
// goroutine both call FreeReference on the same sealed chunk: the second
|
||||
// call must be a no-op, not a double-release.
|
||||
func TestSealedChunk_FreeReferenceIsIdempotent(t *testing.T) {
|
||||
acc := NewWriteBufferAccountant(1024 * 1024)
|
||||
acc.Reserve(64 * 1024)
|
||||
|
||||
sc := &SealedChunk{
|
||||
chunk: NewMemChunk(0, 64*1024),
|
||||
referenceCounter: 1,
|
||||
accountant: acc,
|
||||
chunkSize: 64 * 1024,
|
||||
}
|
||||
|
||||
sc.FreeReference("first")
|
||||
if got := acc.Used(); got != 0 {
|
||||
t.Fatalf("expected 0 used after first FreeReference, got %d", got)
|
||||
}
|
||||
sc.FreeReference("second")
|
||||
if got := acc.Used(); got != 0 {
|
||||
t.Fatalf("expected 0 used after second FreeReference, got %d (double release)", got)
|
||||
}
|
||||
}
|
||||
260
weed/mount/page_writer/upload_pipeline_test.go
Normal file
260
weed/mount/page_writer/upload_pipeline_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
func TestUploadPipeline(t *testing.T) {
|
||||
|
||||
uploadPipeline := NewUploadPipeline(nil, 2*1024*1024, nil, 16, "", nil)
|
||||
|
||||
writeRange(uploadPipeline, 0, 131072)
|
||||
writeRange(uploadPipeline, 131072, 262144)
|
||||
writeRange(uploadPipeline, 262144, 1025536)
|
||||
|
||||
confirmRange(t, uploadPipeline, 0, 1025536)
|
||||
|
||||
writeRange(uploadPipeline, 1025536, 1296896)
|
||||
|
||||
confirmRange(t, uploadPipeline, 1025536, 1296896)
|
||||
|
||||
writeRange(uploadPipeline, 1296896, 2162688)
|
||||
|
||||
confirmRange(t, uploadPipeline, 1296896, 2162688)
|
||||
|
||||
confirmRange(t, uploadPipeline, 1296896, 2162688)
|
||||
}
|
||||
|
||||
// startOff and stopOff must be divided by 4
|
||||
func writeRange(uploadPipeline *UploadPipeline, startOff, stopOff int64) {
|
||||
p := make([]byte, 4)
|
||||
for i := startOff / 4; i < stopOff/4; i += 4 {
|
||||
util.Uint32toBytes(p, uint32(i))
|
||||
uploadPipeline.SaveDataAt(p, i, false, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func confirmRange(t *testing.T, uploadPipeline *UploadPipeline, startOff, stopOff int64) {
|
||||
p := make([]byte, 4)
|
||||
for i := startOff; i < stopOff/4; i += 4 {
|
||||
uploadPipeline.MaybeReadDataAt(p, i, 0)
|
||||
x := util.BytesToUint32(p)
|
||||
if x != uint32(i) {
|
||||
t.Errorf("expecting %d found %d at offset [%d,%d)", i, x, i, i+4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pressure-driven eviction must not seal a chunk with a leading or
|
||||
// internal gap (issue #9330).
|
||||
func TestEvictOneWritableChunk_SkipsGappyChunks(t *testing.T) {
|
||||
const cs int64 = 2 * 1024 * 1024
|
||||
// nil saveToStorage so the async upload SaveContent is a no-op.
|
||||
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(2), cs, nil, 16, "", nil)
|
||||
|
||||
block := make([]byte, cs/4)
|
||||
|
||||
// chunk 0: internal gap; chunk 1: leading gap; chunk 2: contiguous from 0.
|
||||
if _, err := up.SaveDataAt(block, 0, true, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, 3*cs/4, true, 2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs+cs/4, true, 3); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs+cs/2, true, 4); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, 2*cs, true, 5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, 2*cs+cs/4, true, 6); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !up.EvictOneWritableChunk() {
|
||||
t.Fatalf("EvictOneWritableChunk returned false; expected contiguous chunk 2 to be evictable")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(2)]; stillWritable {
|
||||
t.Errorf("chunk 2 should have moved to sealed")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; !stillWritable {
|
||||
t.Errorf("chunk 0 (internal gap) must remain writable")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; !stillWritable {
|
||||
t.Errorf("chunk 1 (leading gap) must remain writable")
|
||||
}
|
||||
|
||||
// Filling holes makes IsComplete true; maybeMoveToSealed auto-seals.
|
||||
if _, err := up.SaveDataAt(block, cs/4, true, 7); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs/2, true, 8); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs, true, 9); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs+3*cs/4, true, 10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; stillWritable {
|
||||
t.Errorf("chunk 0 should have auto-sealed after gap was filled")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; stillWritable {
|
||||
t.Errorf("chunk 1 should have auto-sealed after leading range was filled")
|
||||
}
|
||||
}
|
||||
|
||||
// When every chunk is gappy, the fallback must still seal one so
|
||||
// accountant.Reserve can wake; oldest-LastWriteTsNs wins.
|
||||
func TestEvictOneWritableChunk_FallbackPicksOldestGappy(t *testing.T) {
|
||||
const cs int64 = 2 * 1024 * 1024
|
||||
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(2), cs, nil, 16, "", nil)
|
||||
|
||||
block := make([]byte, cs/4)
|
||||
|
||||
// Strictly increasing tsNs so chunk 0 is oldest; equal WrittenSize.
|
||||
if _, err := up.SaveDataAt(block, 0, true, 100); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, 3*cs/4, true, 101); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs+cs/4, true, 200); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs+cs/2, true, 201); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, 2*cs, true, 300); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, 2*cs+3*cs/4, true, 301); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !up.EvictOneWritableChunk() {
|
||||
t.Fatalf("fallback must seal a gappy chunk to free a Reserve slot")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; stillWritable {
|
||||
t.Errorf("oldest gappy chunk (0) should have been picked by the fallback")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; !stillWritable {
|
||||
t.Errorf("chunk 1 should remain writable")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(2)]; !stillWritable {
|
||||
t.Errorf("chunk 2 should remain writable")
|
||||
}
|
||||
}
|
||||
|
||||
// Strict pass preempts the fallback even when a gappy chunk is older.
|
||||
func TestEvictOneWritableChunk_PrefersStrictOverFallback(t *testing.T) {
|
||||
const cs int64 = 2 * 1024 * 1024
|
||||
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(2), cs, nil, 16, "", nil)
|
||||
|
||||
block := make([]byte, cs/4)
|
||||
|
||||
// chunk 0 (older, gappy), chunk 1 (newer, contiguous from 0).
|
||||
if _, err := up.SaveDataAt(block, 0, true, 100); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, 3*cs/4, true, 101); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs, true, 200); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs+cs/4, true, 201); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !up.EvictOneWritableChunk() {
|
||||
t.Fatalf("EvictOneWritableChunk returned false")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; stillWritable {
|
||||
t.Errorf("contiguous chunk 1 must be picked over older gappy chunk 0")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; !stillWritable {
|
||||
t.Errorf("gappy chunk 0 must remain writable")
|
||||
}
|
||||
}
|
||||
|
||||
// ProactiveFlush also skips gappy chunks (issue #9330). No liveness
|
||||
// fallback here — unlike EvictOneWritableChunk, returning false is just
|
||||
// a missed optimization.
|
||||
func TestProactiveFlush_SkipsGappyChunks(t *testing.T) {
|
||||
const cs int64 = 2 * 1024 * 1024
|
||||
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(2), cs, nil, 16, "", nil)
|
||||
|
||||
block := make([]byte, cs/4)
|
||||
|
||||
// chunk 0: internal gap; chunk 1: leading gap; chunk 2: contiguous from 0.
|
||||
if _, err := up.SaveDataAt(block, 0, true, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, 3*cs/4, true, 2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs+cs/4, true, 3); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs+cs/2, true, 4); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, 2*cs, true, 5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, 2*cs+cs/4, true, 6); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// nowNs >> chunk timestamps so age clears the idle/maxHold gates;
|
||||
// fillRatio < WrittenSize so nearlyFull is true. Leaves
|
||||
// IsContiguouslyWritten as the only differentiator.
|
||||
const (
|
||||
nowNs int64 = 1_000_000_000
|
||||
idleThresholdNs int64 = 100
|
||||
maxHoldNs int64 = 200
|
||||
fillRatio int64 = cs / 8
|
||||
)
|
||||
if !up.ProactiveFlush(nowNs, idleThresholdNs, maxHoldNs, fillRatio, 0, false) {
|
||||
t.Fatalf("ProactiveFlush returned false; expected contiguous chunk 2 to be sealed")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(2)]; stillWritable {
|
||||
t.Errorf("chunk 2 should have moved to sealed")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; !stillWritable {
|
||||
t.Errorf("chunk 0 (internal gap) must remain writable")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; !stillWritable {
|
||||
t.Errorf("chunk 1 (leading gap) must remain writable")
|
||||
}
|
||||
|
||||
if up.ProactiveFlush(nowNs, idleThresholdNs, maxHoldNs, fillRatio, 0, false) {
|
||||
t.Errorf("ProactiveFlush returned true with only gappy chunks left")
|
||||
}
|
||||
|
||||
if _, err := up.SaveDataAt(block, cs/4, true, 7); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs/2, true, 8); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs, true, 9); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := up.SaveDataAt(block, cs+3*cs/4, true, 10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; stillWritable {
|
||||
t.Errorf("chunk 0 should have auto-sealed after gap was filled")
|
||||
}
|
||||
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; stillWritable {
|
||||
t.Errorf("chunk 1 should have auto-sealed after leading range was filled")
|
||||
}
|
||||
}
|
||||
180
weed/mount/page_writer/write_buffer_accountant.go
Normal file
180
weed/mount/page_writer/write_buffer_accountant.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// softThresholdRatio is the fraction of cap at which soft throttling begins.
|
||||
// At this level, writes sleep briefly to let uploaders drain.
|
||||
softThresholdRatio = 0.8
|
||||
// hardThresholdRatio is the fraction of cap at which hard throttling kicks in.
|
||||
// Writes sleep longer to aggressively slow intake.
|
||||
hardThresholdRatio = 0.95
|
||||
// softThrottleDelay is the sleep duration when usage exceeds the soft threshold.
|
||||
softThrottleDelay = 10 * time.Millisecond
|
||||
// hardThrottleDelay is the sleep duration when usage exceeds the hard threshold.
|
||||
hardThrottleDelay = 50 * time.Millisecond
|
||||
)
|
||||
|
||||
// WriteBufferAccountant enforces a global byte budget across all
|
||||
// UploadPipeline instances. Callers Reserve chunk-sized slots before
|
||||
// allocating a page chunk (in memory or on swap) and Release them when
|
||||
// the chunk is freed. Reserve blocks when the cap would be exceeded,
|
||||
// providing natural backpressure to the FUSE write path when volume
|
||||
// uploads stall (e.g. all assigned volumes are full) instead of letting
|
||||
// the swap file grow without bound.
|
||||
//
|
||||
// A nil receiver is treated as "unlimited" for backward compatibility.
|
||||
type WriteBufferAccountant struct {
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
cap int64 // 0 means unlimited
|
||||
used int64
|
||||
softThreshold int64 // pre-computed: cap * softThresholdRatio
|
||||
hardThreshold int64 // pre-computed: cap * hardThresholdRatio
|
||||
evictor func(needBytes int64) bool
|
||||
evicting bool
|
||||
|
||||
softThrottleCount atomic.Int64
|
||||
hardThrottleCount atomic.Int64
|
||||
}
|
||||
|
||||
func NewWriteBufferAccountant(capBytes int64) *WriteBufferAccountant {
|
||||
a := &WriteBufferAccountant{
|
||||
cap: capBytes,
|
||||
softThreshold: int64(float64(capBytes) * softThresholdRatio),
|
||||
hardThreshold: int64(float64(capBytes) * hardThresholdRatio),
|
||||
}
|
||||
a.cond = sync.NewCond(&a.mu)
|
||||
return a
|
||||
}
|
||||
|
||||
// SetEvictor registers a callback that Reserve invokes when the cap would
|
||||
// otherwise block. The evictor is expected to force-seal at least one
|
||||
// writable chunk in some UploadPipeline, which turns a pinned-forever
|
||||
// writable chunk into a sealed chunk that the async uploader drains and
|
||||
// Releases. Without this hook, workloads that hold many files open for
|
||||
// write with less-than-chunkSize data in each (e.g. fio 4k randwrite with
|
||||
// nrfiles * chunkSize > cap) deadlock permanently, because writable chunks
|
||||
// only seal on close or when they fill.
|
||||
//
|
||||
// The evictor must not call Reserve on the same accountant or re-enter
|
||||
// Reserve transitively — it would deadlock on accountant.mu.
|
||||
func (a *WriteBufferAccountant) SetEvictor(fn func(needBytes int64) bool) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.evictor = fn
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// Reserve blocks until n bytes can be accounted for under the cap.
|
||||
// It must not be called while holding any UploadPipeline lock, or the
|
||||
// uploader goroutines that eventually call Release will deadlock.
|
||||
func (a *WriteBufferAccountant) Reserve(n int64) {
|
||||
if a == nil || a.cap <= 0 {
|
||||
return
|
||||
}
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
// Graduated backpressure: slow writers before hitting the hard cap.
|
||||
// Use projected usage (used + n) so that a single reservation crossing
|
||||
// a threshold is throttled immediately rather than on the next call.
|
||||
//
|
||||
// This runs once per Reserve, not inside the blocking loop below.
|
||||
// Once usage reaches the cap, the evictor + cond.Wait is the correct
|
||||
// mechanism: it blocks until a sealed chunk upload frees space, which
|
||||
// is strictly more efficient than repeated time-based sleeps that
|
||||
// cannot free capacity on their own.
|
||||
projected := a.used + n
|
||||
if projected >= a.hardThreshold && a.used > 0 {
|
||||
a.hardThrottleCount.Add(1)
|
||||
a.mu.Unlock()
|
||||
time.Sleep(hardThrottleDelay)
|
||||
a.mu.Lock()
|
||||
} else if projected >= a.softThreshold && a.used > 0 {
|
||||
a.softThrottleCount.Add(1)
|
||||
a.mu.Unlock()
|
||||
time.Sleep(softThrottleDelay)
|
||||
a.mu.Lock()
|
||||
}
|
||||
|
||||
for a.used+n > a.cap && a.used > 0 {
|
||||
// Before blocking, try to force-seal a writable chunk somewhere so
|
||||
// its async upload path will eventually Release a slot. Single-flight
|
||||
// on `evicting` so a stampede of blocked reservers doesn't iterate
|
||||
// the fhMap concurrently.
|
||||
if a.evictor != nil && !a.evicting {
|
||||
a.runEvictorLocked(n)
|
||||
// A concurrent Release may have brought used back under the
|
||||
// cap during the evict window — re-check before waiting so we
|
||||
// do not block on a broadcast that has already fired.
|
||||
if a.used+n <= a.cap || a.used == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
a.cond.Wait()
|
||||
}
|
||||
a.used += n
|
||||
}
|
||||
|
||||
// runEvictorLocked is called with a.mu held and !a.evicting. It drops the
|
||||
// lock around the evictor invocation so uploader goroutines (which call
|
||||
// Release under the same lock) can make progress, and uses defer to
|
||||
// guarantee the `evicting` flag and the lock are restored even if the
|
||||
// evictor panics.
|
||||
func (a *WriteBufferAccountant) runEvictorLocked(n int64) {
|
||||
evictor := a.evictor
|
||||
a.evicting = true
|
||||
a.mu.Unlock()
|
||||
defer func() {
|
||||
a.mu.Lock()
|
||||
a.evicting = false
|
||||
a.cond.Broadcast()
|
||||
}()
|
||||
evictor(n)
|
||||
}
|
||||
|
||||
func (a *WriteBufferAccountant) Release(n int64) {
|
||||
if a == nil || a.cap <= 0 {
|
||||
return
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.used -= n
|
||||
if a.used < 0 {
|
||||
a.used = 0
|
||||
}
|
||||
a.cond.Broadcast()
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// Used returns the currently reserved byte count (for tests/metrics).
|
||||
func (a *WriteBufferAccountant) Used() int64 {
|
||||
if a == nil || a.cap <= 0 {
|
||||
return 0
|
||||
}
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return a.used
|
||||
}
|
||||
|
||||
// SoftThrottleCount returns the number of times soft throttling was triggered.
|
||||
func (a *WriteBufferAccountant) SoftThrottleCount() int64 {
|
||||
if a == nil {
|
||||
return 0
|
||||
}
|
||||
return a.softThrottleCount.Load()
|
||||
}
|
||||
|
||||
// HardThrottleCount returns the number of times hard throttling was triggered.
|
||||
func (a *WriteBufferAccountant) HardThrottleCount() int64 {
|
||||
if a == nil {
|
||||
return 0
|
||||
}
|
||||
return a.hardThrottleCount.Load()
|
||||
}
|
||||
199
weed/mount/page_writer/write_buffer_accountant_test.go
Normal file
199
weed/mount/page_writer/write_buffer_accountant_test.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWriteBufferAccountant_Unlimited(t *testing.T) {
|
||||
a := NewWriteBufferAccountant(0)
|
||||
a.Reserve(1 << 30)
|
||||
a.Reserve(1 << 30)
|
||||
if got := a.Used(); got != 0 {
|
||||
t.Fatalf("unlimited accountant should not track usage, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBufferAccountant_Nil(t *testing.T) {
|
||||
var a *WriteBufferAccountant
|
||||
a.Reserve(100) // must not panic
|
||||
a.Release(100)
|
||||
if a.Used() != 0 {
|
||||
t.Fatal("nil Used should return 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBufferAccountant_ReserveAndRelease(t *testing.T) {
|
||||
a := NewWriteBufferAccountant(300)
|
||||
a.Reserve(100)
|
||||
a.Reserve(100)
|
||||
if got := a.Used(); got != 200 {
|
||||
t.Fatalf("expected 200 used, got %d", got)
|
||||
}
|
||||
a.Release(100)
|
||||
if got := a.Used(); got != 100 {
|
||||
t.Fatalf("expected 100 used after release, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBufferAccountant_BlocksWhenOverCap(t *testing.T) {
|
||||
a := NewWriteBufferAccountant(100)
|
||||
a.Reserve(100)
|
||||
|
||||
started := make(chan struct{})
|
||||
var landed atomic.Bool
|
||||
go func() {
|
||||
close(started)
|
||||
a.Reserve(50) // should block — used=100, cap=100
|
||||
landed.Store(true)
|
||||
}()
|
||||
|
||||
// Wait until the goroutine is about to enter Reserve. Once it does,
|
||||
// Reserve cannot make progress until Release is called (cap is
|
||||
// full), so landed is guaranteed to stay false until we release.
|
||||
<-started
|
||||
if landed.Load() {
|
||||
t.Fatal("second reserve should be blocked while cap is full")
|
||||
}
|
||||
|
||||
a.Release(100)
|
||||
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for !landed.Load() && time.Now().Before(deadline) {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
if !landed.Load() {
|
||||
t.Fatal("second reserve should unblock after release")
|
||||
}
|
||||
if got := a.Used(); got != 50 {
|
||||
t.Fatalf("expected 50 used after handoff, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBufferAccountant_SoftThrottle(t *testing.T) {
|
||||
// Cap = 1000, soft threshold = 800. Reserve 850 (85%) then measure
|
||||
// that the next Reserve is delayed by at least softThrottleDelay.
|
||||
a := NewWriteBufferAccountant(1000)
|
||||
a.Reserve(850)
|
||||
|
||||
start := time.Now()
|
||||
a.Reserve(10) // should trigger soft throttle (used=850 > 800)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if elapsed < softThrottleDelay {
|
||||
t.Fatalf("expected Reserve to take >= %v (soft throttle), took %v", softThrottleDelay, elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBufferAccountant_HardThrottle(t *testing.T) {
|
||||
// Cap = 1000, hard threshold = 950. Reserve 960 (96%) then measure
|
||||
// that the next Reserve is delayed by at least hardThrottleDelay.
|
||||
a := NewWriteBufferAccountant(1000)
|
||||
a.Reserve(960)
|
||||
|
||||
start := time.Now()
|
||||
a.Reserve(10) // should trigger hard throttle (used=960 > 950)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if elapsed < hardThrottleDelay {
|
||||
t.Fatalf("expected Reserve to take >= %v (hard throttle), took %v", hardThrottleDelay, elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBufferAccountant_NoThrottleBelowSoft(t *testing.T) {
|
||||
// Cap = 1000, soft threshold = 800. Reserve 700 (70%) -- below soft
|
||||
// threshold, so subsequent Reserve should return quickly.
|
||||
a := NewWriteBufferAccountant(1000)
|
||||
a.Reserve(700)
|
||||
|
||||
start := time.Now()
|
||||
a.Reserve(10)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if elapsed >= softThrottleDelay {
|
||||
t.Fatalf("expected Reserve below soft threshold to avoid throttle delay (< %v), took %v", softThrottleDelay, elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBufferAccountant_ThrottleCounters(t *testing.T) {
|
||||
a := NewWriteBufferAccountant(1000)
|
||||
|
||||
// Below soft -- no throttle
|
||||
a.Reserve(700)
|
||||
a.Reserve(10)
|
||||
if a.SoftThrottleCount() != 0 || a.HardThrottleCount() != 0 {
|
||||
t.Fatalf("expected no throttle counts below soft threshold, got soft=%d hard=%d",
|
||||
a.SoftThrottleCount(), a.HardThrottleCount())
|
||||
}
|
||||
|
||||
// Release back down, then fill to soft zone
|
||||
a.Release(710)
|
||||
a.Reserve(850) // now used=850, above soft (800)
|
||||
a.Reserve(10) // triggers soft throttle
|
||||
if a.SoftThrottleCount() != 1 {
|
||||
t.Fatalf("expected soft throttle count 1, got %d", a.SoftThrottleCount())
|
||||
}
|
||||
if a.HardThrottleCount() != 0 {
|
||||
t.Fatalf("expected hard throttle count 0, got %d", a.HardThrottleCount())
|
||||
}
|
||||
|
||||
// Release and fill to hard zone
|
||||
a.Release(860)
|
||||
a.Reserve(960) // now used=960, above hard (950)
|
||||
a.Reserve(10) // triggers hard throttle
|
||||
if a.HardThrottleCount() != 1 {
|
||||
t.Fatalf("expected hard throttle count 1, got %d", a.HardThrottleCount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBufferAccountant_GraduatedRecovery(t *testing.T) {
|
||||
// Fill to hard threshold, verify hard throttle. Release to soft zone,
|
||||
// verify that throttle level decreases to soft.
|
||||
a := NewWriteBufferAccountant(1000)
|
||||
|
||||
// Fill to hard zone (960 > 950)
|
||||
a.Reserve(960)
|
||||
start := time.Now()
|
||||
a.Reserve(10) // hard throttle
|
||||
elapsed := time.Since(start)
|
||||
if elapsed < hardThrottleDelay {
|
||||
t.Fatalf("expected hard throttle delay, got %v", elapsed)
|
||||
}
|
||||
if a.HardThrottleCount() != 1 {
|
||||
t.Fatalf("expected hard throttle count 1, got %d", a.HardThrottleCount())
|
||||
}
|
||||
|
||||
// Release down to soft zone: used = 970 - 130 = 840 (> 800 soft, < 950 hard)
|
||||
a.Release(130)
|
||||
|
||||
start = time.Now()
|
||||
a.Reserve(10) // should now be soft throttle
|
||||
elapsed = time.Since(start)
|
||||
if elapsed < softThrottleDelay {
|
||||
t.Fatalf("expected soft throttle delay after recovery, got %v", elapsed)
|
||||
}
|
||||
if a.HardThrottleCount() != 1 {
|
||||
t.Fatalf("expected hard throttle count to remain 1 after recovery, got %d", a.HardThrottleCount())
|
||||
}
|
||||
if a.SoftThrottleCount() != 1 {
|
||||
t.Fatalf("expected soft throttle count 1 after recovery, got %d", a.SoftThrottleCount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBufferAccountant_AllowsOversizeWhenEmpty(t *testing.T) {
|
||||
// A single request larger than cap must succeed when nothing is in
|
||||
// flight, otherwise a single file handle with a large chunk size would
|
||||
// deadlock forever.
|
||||
a := NewWriteBufferAccountant(100)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
a.Reserve(500)
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("oversize reserve on empty accountant should not block")
|
||||
}
|
||||
}
|
||||
175
weed/mount/page_writer/write_buffer_cap_integration_test.go
Normal file
175
weed/mount/page_writer/write_buffer_cap_integration_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package page_writer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// TestWriteBufferCap_SharedAcrossPipelines exercises the full write-budget
|
||||
// plumbing end-to-end: one WriteBufferAccountant shared across several
|
||||
// UploadPipeline instances (modeling multiple open file handles in a
|
||||
// single mount), a deliberately stalled saveFn (modeling the reported
|
||||
// "volume server rejecting uploads" condition), and many concurrent
|
||||
// writers. Without the cap this scenario is what filled /tmp to 1.8 TB;
|
||||
// with the cap, Used() must never exceed the configured budget and
|
||||
// writers must backpressure instead of allocating unboundedly.
|
||||
func TestWriteBufferCap_SharedAcrossPipelines(t *testing.T) {
|
||||
const (
|
||||
chunkSize = 64 * 1024
|
||||
capChunks = 4 // global budget: 4 chunks across all pipelines
|
||||
numPipelines = 3
|
||||
numWriters = 6
|
||||
writesEach = 8 // more than enough to exceed capChunks if unbounded
|
||||
)
|
||||
capBytes := int64(capChunks * chunkSize)
|
||||
acc := NewWriteBufferAccountant(capBytes)
|
||||
|
||||
// Gate every upload so sealed chunks cannot drain until the test says
|
||||
// so — this is what reproduces the "stalled volume server" scenario.
|
||||
var inflight atomic.Int64
|
||||
var maxInflight atomic.Int64
|
||||
release := make(chan struct{})
|
||||
saveFn := func(_ io.Reader, _, _, _ int64, cleanup func()) {
|
||||
cur := inflight.Add(1)
|
||||
for {
|
||||
prev := maxInflight.Load()
|
||||
if cur <= prev || maxInflight.CompareAndSwap(prev, cur) {
|
||||
break
|
||||
}
|
||||
}
|
||||
<-release
|
||||
inflight.Add(-1)
|
||||
if cleanup != nil {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// Sample Used() frequently on a background goroutine so we can catch
|
||||
// any moment the accountant exceeds the cap.
|
||||
var observedMax atomic.Int64
|
||||
stopSampling := make(chan struct{})
|
||||
var samplerWg sync.WaitGroup
|
||||
samplerWg.Add(1)
|
||||
go func() {
|
||||
defer samplerWg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-stopSampling:
|
||||
return
|
||||
default:
|
||||
}
|
||||
u := acc.Used()
|
||||
for {
|
||||
prev := observedMax.Load()
|
||||
if u <= prev || observedMax.CompareAndSwap(prev, u) {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
// Build N pipelines, each with its own uploader executor so chunks
|
||||
// can actually enter the upload stage and hit the gated saveFn.
|
||||
pipelines := make([]*UploadPipeline, numPipelines)
|
||||
for i := range pipelines {
|
||||
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(4), chunkSize, saveFn, 2, t.TempDir(), acc)
|
||||
pipelines[i] = up
|
||||
}
|
||||
|
||||
// Launch numWriters goroutines, round-robin across pipelines. Each
|
||||
// writer writes writesEach full chunks, on distinct logical chunk
|
||||
// indices so every write triggers a new reservation.
|
||||
var writerWg sync.WaitGroup
|
||||
writerStart := make(chan struct{})
|
||||
writerWg.Add(numWriters)
|
||||
for w := 0; w < numWriters; w++ {
|
||||
w := w
|
||||
go func() {
|
||||
defer writerWg.Done()
|
||||
<-writerStart
|
||||
up := pipelines[w%numPipelines]
|
||||
data := make([]byte, chunkSize)
|
||||
for k := 0; k < writesEach; k++ {
|
||||
// Offset each writer into a distinct region of the file
|
||||
// so chunks don't collide on the same logicChunkIndex.
|
||||
off := int64(w)*int64(writesEach)*chunkSize + int64(k)*chunkSize
|
||||
if _, err := up.SaveDataAt(data, off, true, int64(k+1)); err != nil {
|
||||
t.Errorf("writer %d: SaveDataAt: %v", w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
close(writerStart)
|
||||
|
||||
// Give the writers time to pile up against the cap. Any unbounded
|
||||
// allocation would show up as Used() > capBytes; the sampler catches
|
||||
// that. Writers should be blocked on Reserve well before writesEach
|
||||
// chunks are created per pipeline.
|
||||
deadline := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
if acc.Used() >= capBytes {
|
||||
break
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
if got := acc.Used(); got < capBytes/2 {
|
||||
t.Fatalf("expected writers to fill the budget, used=%d cap=%d", got, capBytes)
|
||||
}
|
||||
|
||||
// While the gate is closed and writers are blocked, Used() must stay
|
||||
// at or below the cap. Sample explicitly for a short window to catch
|
||||
// any overshoot.
|
||||
for i := 0; i < 50; i++ {
|
||||
if u := acc.Used(); u > capBytes {
|
||||
t.Fatalf("Used()=%d exceeded cap=%d while writers were stalled", u, capBytes)
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Open the gate: every sealed chunk completes, writers drain.
|
||||
close(release)
|
||||
|
||||
// Wait for all writers to finish.
|
||||
writerDone := make(chan struct{})
|
||||
go func() {
|
||||
writerWg.Wait()
|
||||
close(writerDone)
|
||||
}()
|
||||
select {
|
||||
case <-writerDone:
|
||||
case <-time.After(10 * time.Second):
|
||||
close(stopSampling)
|
||||
samplerWg.Wait()
|
||||
t.Fatalf("writers did not drain after release; used=%d", acc.Used())
|
||||
}
|
||||
|
||||
// Shut down all pipelines and stop sampling.
|
||||
for _, up := range pipelines {
|
||||
up.Shutdown()
|
||||
}
|
||||
close(stopSampling)
|
||||
samplerWg.Wait()
|
||||
|
||||
if got := observedMax.Load(); got > capBytes {
|
||||
t.Fatalf("observed Used()=%d exceeded cap=%d", got, capBytes)
|
||||
}
|
||||
// An uploader goroutine releases its budget slot only after reacquiring
|
||||
// chunksLock post-Shutdown, so the last releases may land asynchronously.
|
||||
drainDeadline := time.Now().Add(5 * time.Second)
|
||||
for acc.Used() != 0 && time.Now().Before(drainDeadline) {
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
if got := acc.Used(); got != 0 {
|
||||
t.Fatalf("expected 0 used after shutdown, got %d", got)
|
||||
}
|
||||
|
||||
t.Logf("peak inflight uploads=%d, peak Used()=%d bytes (cap=%d)",
|
||||
maxInflight.Load(), observedMax.Load(), capBytes)
|
||||
}
|
||||
Reference in New Issue
Block a user