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:
579
weed/storage/erasure_coding/ecbalancer/place.go
Normal file
579
weed/storage/erasure_coding/ecbalancer/place.go
Normal file
@@ -0,0 +1,579 @@
|
||||
package ecbalancer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
||||
storagetypes "github.com/seaweedfs/seaweedfs/weed/storage/types"
|
||||
)
|
||||
|
||||
// Constraints configures a Place call. Ratio resolves a collection's
|
||||
// (dataShards, parityShards); nil uses the standard scheme. ReplicaPlacement,
|
||||
// when non-nil, caps shards per rack (DiffRackCount = max shards/rack) and per
|
||||
// node within a rack (SameRackCount = max shards/node); both digits are direct
|
||||
// hard caps. The data-center digit (DiffDataCenterCount) is not honored:
|
||||
// the 1-byte volume ReplicaPlacement can only encode 0-2 there, too small to be a
|
||||
// meaningful per-DC EC shard cap, so EC relies on the rack/node even spread instead.
|
||||
//
|
||||
// DiskTypePolicy controls how DiskType constrains placement (Any / Prefer /
|
||||
// Require). PreferredTags drives whole-plan tag tiering: Place tries disks
|
||||
// carrying the earliest tags first and widens to all disks only if a tier cannot
|
||||
// place every shard.
|
||||
type Constraints struct {
|
||||
DiskType string
|
||||
DiskTypePolicy DiskTypePolicy
|
||||
PreferredTags []string
|
||||
ReplicaPlacement *super_block.ReplicaPlacement
|
||||
Ratio func(collection string) (dataShards, parityShards int)
|
||||
}
|
||||
|
||||
// DiskTypePolicy controls how Constraints.DiskType constrains placement.
|
||||
type DiskTypePolicy int
|
||||
|
||||
const (
|
||||
DiskTypeAny DiskTypePolicy = iota // any disk type
|
||||
DiskTypePrefer // prefer DiskType, spill to other types if needed
|
||||
DiskTypeRequire // only DiskType (HardDriveType when "")
|
||||
)
|
||||
|
||||
// diskTypeEqual compares disk types after normalization, so "" and "hdd" (both
|
||||
// HardDriveType) are equal.
|
||||
func diskTypeEqual(a, b string) bool {
|
||||
return storagetypes.ToDiskType(a).String() == storagetypes.ToDiskType(b).String()
|
||||
}
|
||||
|
||||
// diskHasAnyTag reports whether the disk carries any of the given tags.
|
||||
func diskHasAnyTag(d *disk, tags []string) bool {
|
||||
for _, want := range tags {
|
||||
for _, have := range d.tags {
|
||||
if have == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Destination is a chosen target for one shard. DataCenter and Rack are kept as
|
||||
// separate values (matching topology.DiskInfo) rather than a "dc:rack" composite,
|
||||
// so callers read them directly instead of parsing.
|
||||
type Destination struct {
|
||||
Node string
|
||||
DiskID uint32
|
||||
DataCenter string
|
||||
Rack string // bare rack id within DataCenter
|
||||
}
|
||||
|
||||
// PlaceResult holds the chosen destinations, which constraints had to be relaxed
|
||||
// (durability-first only), and whether placement spilled outside the preferred
|
||||
// disk type or tag tiers (for parity with today's logging).
|
||||
type PlaceResult struct {
|
||||
Destinations map[int]Destination
|
||||
Relaxed []string
|
||||
SpilledToOtherDiskType bool
|
||||
SpilledOutsidePreferredTags bool
|
||||
}
|
||||
|
||||
// PlacementMode selects the strictness/relaxation policy.
|
||||
type PlacementMode int
|
||||
|
||||
const (
|
||||
// PlaceStrict: caps and ReplicaPlacement are hard. Place fails rather than
|
||||
// violate them, so the caller can defer (leave the volume as-is and retry).
|
||||
PlaceStrict PlacementMode = iota
|
||||
// PlaceDurabilityFirst (used by both encode and repair): relax per-type caps ->
|
||||
// data/parity anti-affinity -> ReplicaPlacement, in that order, until each shard
|
||||
// lands, reporting what was relaxed in PlaceResult.Relaxed. The per-disk
|
||||
// durability cap (<= parityShards per disk) is never relaxed. Fails only if no
|
||||
// disk has free capacity. Encode places best-effort this way and rebalancing
|
||||
// tightens the spread afterward.
|
||||
PlaceDurabilityFirst
|
||||
)
|
||||
|
||||
// relaxation controls which placement-quality constraints are enforced on an
|
||||
// attempt. preferring fresh nodes (repair's "avoid surviving-shard nodes") is not
|
||||
// listed: pickNodeInRack already selects the node with the fewest shards of the
|
||||
// volume, so survivors are deprioritized with built-in fallback.
|
||||
type relaxation struct {
|
||||
caps bool
|
||||
antiAffinity bool
|
||||
rp bool
|
||||
}
|
||||
|
||||
func (r relaxation) relaxedNames() []string {
|
||||
var n []string
|
||||
if !r.caps {
|
||||
n = append(n, "caps")
|
||||
}
|
||||
if !r.antiAffinity {
|
||||
n = append(n, "anti-affinity")
|
||||
}
|
||||
if !r.rp {
|
||||
n = append(n, "replica-placement")
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
var strictAttempts = []relaxation{{caps: true, antiAffinity: true, rp: true}}
|
||||
|
||||
var durabilityAttempts = []relaxation{
|
||||
{caps: true, antiAffinity: true, rp: true},
|
||||
{caps: false, antiAffinity: true, rp: true},
|
||||
{caps: false, antiAffinity: false, rp: true},
|
||||
{caps: false, antiAffinity: false, rp: false},
|
||||
}
|
||||
|
||||
type placedEntry struct {
|
||||
node *Node
|
||||
sid int
|
||||
rackKey string
|
||||
}
|
||||
|
||||
// Place assigns destinations for the `need` shard ids of volume (collection,vid),
|
||||
// reading the volume's already-placed shards from the snapshot (so encode passes
|
||||
// an empty-for-this-volume snapshot, repair passes one seeded with the surviving
|
||||
// shards).
|
||||
//
|
||||
// Tag tiering (whole-plan retry): it tries the preferred-tag tiers in order, each
|
||||
// a complete candidate set, and returns the first tier that places every shard;
|
||||
// only when it falls through to the no-tag tier does it set
|
||||
// SpilledOutsidePreferredTags. Within a tier, disk-type Prefer spills to other
|
||||
// types per shard (SpilledToOtherDiskType); Require filters strictly.
|
||||
func (t *Topology) Place(vid uint32, collection string, need []int, c Constraints, mode PlacementMode) (*PlaceResult, error) {
|
||||
if len(need) == 0 {
|
||||
return &PlaceResult{Destinations: map[int]Destination{}}, nil
|
||||
}
|
||||
|
||||
vk := volKey{collection: collection, vid: vid}
|
||||
dataShards, parityShards := erasure_coding.DataShardsCount, erasure_coding.ParityShardsCount
|
||||
if c.Ratio != nil {
|
||||
if d, p := c.Ratio(collection); d > 0 && p > 0 {
|
||||
dataShards, parityShards = d, p
|
||||
}
|
||||
}
|
||||
|
||||
racks := buildRacks(t.nodes)
|
||||
if len(racks) == 0 {
|
||||
return nil, fmt.Errorf("no racks available for EC placement")
|
||||
}
|
||||
rackKeys := sortedKeys(racks)
|
||||
|
||||
// Disk-type eligibility (Require filters; Any/Prefer admit all) and the soft
|
||||
// type preference applied in scoring under Prefer.
|
||||
typeEligible := func(d *disk) bool {
|
||||
if c.DiskTypePolicy == DiskTypeRequire {
|
||||
return diskTypeEqual(d.diskType, c.DiskType)
|
||||
}
|
||||
return true
|
||||
}
|
||||
var prefer func(*disk) bool
|
||||
if c.DiskTypePolicy == DiskTypePrefer {
|
||||
prefer = func(d *disk) bool { return diskTypeEqual(d.diskType, c.DiskType) }
|
||||
}
|
||||
|
||||
// Whole-plan retry over preferred-tag tiers; the first tier that places every
|
||||
// shard wins. Reaching the no-tag tier means we spilled outside the tags.
|
||||
tiers := tagTiers(c.PreferredTags)
|
||||
var lastErr error
|
||||
for _, tierTags := range tiers {
|
||||
tt := tierTags
|
||||
eligible := func(d *disk) bool {
|
||||
return typeEligible(d) && (len(tt) == 0 || diskHasAnyTag(d, tt))
|
||||
}
|
||||
res, err := t.tryPlace(vk, need, dataShards, parityShards, racks, rackKeys, mode, c.ReplicaPlacement, eligible, prefer)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if len(c.PreferredTags) > 0 && len(tierTags) == 0 {
|
||||
res.SpilledOutsidePreferredTags = true
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// tagTiers returns the eligibility tag-sets in increasing breadth, ending with an
|
||||
// empty set ("any disk"). Empty preferredTags yields a single any-disk tier.
|
||||
func tagTiers(preferredTags []string) [][]string {
|
||||
if len(preferredTags) == 0 {
|
||||
return [][]string{nil}
|
||||
}
|
||||
tiers := make([][]string, 0, len(preferredTags)+1)
|
||||
for k := range preferredTags {
|
||||
tiers = append(tiers, append([]string(nil), preferredTags[:k+1]...))
|
||||
}
|
||||
return append(tiers, nil)
|
||||
}
|
||||
|
||||
// tryPlace runs one whole-plan placement attempt restricted to disks satisfying
|
||||
// `eligible`, with `prefer` (may be nil) ranking soft-preferred disks first. It
|
||||
// journals reservations and rolls them all back if any shard cannot be placed, so
|
||||
// a failed tier leaves the snapshot unchanged for the next attempt.
|
||||
func (t *Topology) tryPlace(vk volKey, need []int, dataShards, parityShards int, racks map[string]*rack, rackKeys []string, mode PlacementMode, rp *super_block.ReplicaPlacement, eligible func(*disk) bool, prefer func(*disk) bool) (*PlaceResult, error) {
|
||||
result := &PlaceResult{Destinations: make(map[int]Destination, len(need))}
|
||||
|
||||
// Per-type shard ids per rack (even caps), total shard count per rack
|
||||
// (DiffRackCount), and the racks bearing each type (anti-affinity) — all seeded
|
||||
// from the volume's existing shards.
|
||||
shardsPerRack := map[bool]map[string][]int{true: {}, false: {}}
|
||||
rackShardCount := map[string]int{}
|
||||
bearing := map[bool]map[string]bool{true: {}, false: {}}
|
||||
for _, n := range t.nodes {
|
||||
info, ok := n.shards[vk]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for sid := range info.shardBits.All() {
|
||||
s := int(sid)
|
||||
isData := s < dataShards
|
||||
shardsPerRack[isData][n.rack] = append(shardsPerRack[isData][n.rack], s)
|
||||
rackShardCount[n.rack]++
|
||||
bearing[isData][n.rack] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Even per-rack caps divide by racks that actually have an eligible free disk,
|
||||
// not all racks (the snapshot keeps every disk type/tag), so a valid tiered
|
||||
// cluster — e.g. SSDs in only 2 of 4 racks — is not capped impossibly low.
|
||||
numEligibleRacks := 0
|
||||
for _, rk := range rackKeys {
|
||||
if rackHasFreeDisk(racks[rk], eligible) {
|
||||
numEligibleRacks++
|
||||
}
|
||||
}
|
||||
if numEligibleRacks < 1 {
|
||||
numEligibleRacks = 1
|
||||
}
|
||||
|
||||
attempts := strictAttempts
|
||||
if mode == PlaceDurabilityFirst {
|
||||
attempts = durabilityAttempts
|
||||
}
|
||||
|
||||
var journal []placedEntry
|
||||
relaxedSeen := map[string]bool{}
|
||||
spilledType := false
|
||||
|
||||
placeShard := func(sid int, isData bool) bool {
|
||||
typeTotal := dataShards
|
||||
if !isData {
|
||||
typeTotal = parityShards
|
||||
}
|
||||
for _, rl := range attempts {
|
||||
node, diskID, spilled, ok := chooseShardDest(vk, sid, isData, dataShards, typeTotal, numEligibleRacks, parityShards, racks, rackKeys, rp, eligible, prefer, shardsPerRack[isData], rackShardCount, bearing, rl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
reserveShard(node, vk, sid, diskID)
|
||||
node.freeSlots--
|
||||
racks[node.rack].freeSlots--
|
||||
shardsPerRack[isData][node.rack] = append(shardsPerRack[isData][node.rack], sid)
|
||||
rackShardCount[node.rack]++
|
||||
bearing[isData][node.rack] = true
|
||||
journal = append(journal, placedEntry{node: node, sid: sid, rackKey: node.rack})
|
||||
result.Destinations[sid] = Destination{
|
||||
Node: node.id,
|
||||
DiskID: diskID,
|
||||
DataCenter: node.dc,
|
||||
Rack: strings.TrimPrefix(node.rack, node.dc+":"),
|
||||
}
|
||||
if spilled {
|
||||
spilledType = true
|
||||
}
|
||||
for _, name := range rl.relaxedNames() {
|
||||
relaxedSeen[name] = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Data shards first, then parity, so parity can avoid data-bearing racks.
|
||||
for _, isData := range []bool{true, false} {
|
||||
for _, sid := range shardsOfType(need, isData, dataShards) {
|
||||
if placeShard(sid, isData) {
|
||||
continue
|
||||
}
|
||||
for _, e := range journal {
|
||||
releaseShard(e.node, vk, e.sid)
|
||||
e.node.freeSlots++
|
||||
racks[e.rackKey].freeSlots++
|
||||
}
|
||||
return nil, fmt.Errorf("cannot place EC shard %d of volume %d (collection %q)", sid, vk.vid, vk.collection)
|
||||
}
|
||||
}
|
||||
|
||||
result.SpilledToOtherDiskType = spilledType
|
||||
for name := range relaxedSeen {
|
||||
result.Relaxed = append(result.Relaxed, name)
|
||||
}
|
||||
sort.Strings(result.Relaxed)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// chooseShardDest selects a (node, disk) for one shard at the given relaxation
|
||||
// level: pick a rack (even per-type cap + ReplicaPlacement caps + two-pass
|
||||
// anti-affinity to the opposite type), then the least-loaded eligible node, then
|
||||
// the best eligible disk. The third return reports whether the disk spilled off
|
||||
// the soft-preferred type. ok=false when no rack/node/disk fits.
|
||||
func chooseShardDest(vk volKey, sid int, isData bool, dataShards, typeTotal, numEligibleRacks, maxPerDisk int, racks map[string]*rack, rackKeys []string, rp *super_block.ReplicaPlacement, eligible func(*disk) bool, prefer func(*disk) bool, shardsPerRackType map[string][]int, rackShardCount map[string]int, bearing map[bool]map[string]bool, rl relaxation) (*Node, uint32, bool, bool) {
|
||||
maxPerRack := numEligibleRacks*typeTotal + 1 // effectively unlimited when caps are relaxed
|
||||
if rl.caps {
|
||||
if maxPerRack = ceilDivide(typeTotal, numEligibleRacks); maxPerRack < 1 {
|
||||
maxPerRack = 1
|
||||
}
|
||||
}
|
||||
|
||||
var anti map[string]bool
|
||||
if rl.antiAffinity {
|
||||
anti = bearing[!isData] // racks already holding the opposite shard type
|
||||
}
|
||||
|
||||
if !rl.rp {
|
||||
rp = nil
|
||||
}
|
||||
// A rack is eligible only if it is under the per-rack shard cap (DiffRackCount),
|
||||
// enforced only when set (and relaxed with rp).
|
||||
withinLimit := func(r string) bool {
|
||||
if rp == nil {
|
||||
return true
|
||||
}
|
||||
if rp.DiffRackCount > 0 && rackShardCount[r] >= rp.DiffRackCount {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
destRack, ok := pickTarget(rackKeys, shardsPerRackType, maxPerRack, anti,
|
||||
func(r string) bool { return racks[r].freeSlots > 0 && rackHasFreeDisk(racks[r], eligible) },
|
||||
withinLimit)
|
||||
if !ok {
|
||||
return nil, 0, false, false
|
||||
}
|
||||
node := pickNodeInRackEligible(racks[destRack], vk, rp, eligible)
|
||||
if node == nil {
|
||||
return nil, 0, false, false
|
||||
}
|
||||
diskID, ok, spilled := pickBestDiskEligible(node, vk, eligible, prefer, sid, dataShards, maxPerDisk)
|
||||
if !ok {
|
||||
return nil, 0, false, false
|
||||
}
|
||||
return node, diskID, spilled, true
|
||||
}
|
||||
|
||||
// nodeHasFreeDisk reports whether the node has a free disk satisfying eligible.
|
||||
func nodeHasFreeDisk(n *Node, eligible func(*disk) bool) bool {
|
||||
for _, d := range n.disks {
|
||||
if d.freeSlots > 0 && eligible(d) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// rackHasFreeDisk reports whether any node in the rack has a free eligible disk.
|
||||
func rackHasFreeDisk(r *rack, eligible func(*disk) bool) bool {
|
||||
for _, n := range r.nodes {
|
||||
if n.freeSlots > 0 && nodeHasFreeDisk(n, eligible) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pickNodeInRackEligible is pickNodeInRack restricted to nodes that have a free
|
||||
// eligible disk. FromActiveTopology keeps all disk types/tags in the snapshot, so
|
||||
// without this a node with free volume slots but no eligible disk could be chosen.
|
||||
//
|
||||
// Among eligible nodes it ranks by fewest shards of the volume per machine, then per
|
||||
// node, with free capacity breaking ties. The free-capacity tie-break (not sorted id)
|
||||
// keeps the lowest-id machine from winning every volume's first shard against the
|
||||
// shared encode snapshot and piling up load.
|
||||
func pickNodeInRackEligible(r *rack, vk volKey, rp *super_block.ReplicaPlacement, eligible func(*disk) bool) *Node {
|
||||
machineShards := countShardsByHost(vk, r.nodes)
|
||||
machineFree := freeSlotsByHost(r.nodes)
|
||||
var best *Node
|
||||
var bestMCount, bestMFree, bestNCount, bestNFree int
|
||||
for _, id := range sortedNodeKeys(r.nodes) {
|
||||
node := r.nodes[id]
|
||||
if node.freeSlots <= 0 {
|
||||
continue
|
||||
}
|
||||
if !nodeHasFreeDisk(node, eligible) {
|
||||
continue
|
||||
}
|
||||
count := volumeShardCount(node, vk)
|
||||
if rp != nil && rp.SameRackCount > 0 && count >= rp.SameRackCount {
|
||||
continue
|
||||
}
|
||||
mCount, mFree := machineShards[node.host], machineFree[node.host]
|
||||
better := false
|
||||
switch {
|
||||
case best == nil:
|
||||
better = true
|
||||
case mCount != bestMCount:
|
||||
better = mCount < bestMCount
|
||||
case mFree != bestMFree:
|
||||
better = mFree > bestMFree
|
||||
case count != bestNCount:
|
||||
better = count < bestNCount
|
||||
default:
|
||||
better = node.freeSlots > bestNFree
|
||||
}
|
||||
if better {
|
||||
best, bestMCount, bestMFree, bestNCount, bestNFree = node, mCount, mFree, count, node.freeSlots
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// pickBestDiskEligible chooses the best eligible disk on a node, ranking
|
||||
// soft-preferred disks (prefer != nil && prefer(d)) ahead of others so disk-type
|
||||
// Prefer uses the preferred type when available but spills otherwise. Returns the
|
||||
// disk id, whether one was found, and whether the chosen disk spilled off the
|
||||
// preferred type.
|
||||
func pickBestDiskEligible(node *Node, vk volKey, eligible func(*disk) bool, prefer func(*disk) bool, shardID, dataShardCount, maxPerDisk int) (uint32, bool, bool) {
|
||||
isDataShard := dataShardCount > 0 && shardID < dataShardCount
|
||||
info := node.shards[vk]
|
||||
var bestDiskID uint32
|
||||
bestScore := -1
|
||||
bestPreferred := false
|
||||
for _, diskID := range sortedDiskKeys(node.disks) {
|
||||
d := node.disks[diskID]
|
||||
if !eligible(d) || d.freeSlots <= 0 {
|
||||
continue
|
||||
}
|
||||
existingShards := 0
|
||||
hasData := false
|
||||
hasParity := false
|
||||
if info != nil {
|
||||
bits := info.diskShardBits[diskID]
|
||||
existingShards = bits.Count()
|
||||
if dataShardCount > 0 {
|
||||
for sid := range bits.All() {
|
||||
if int(sid) < dataShardCount {
|
||||
hasData = true
|
||||
} else {
|
||||
hasParity = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Durability: never put more than maxPerDisk (parityShards) shards of this
|
||||
// volume on one disk, or losing that disk would lose more than EC can
|
||||
// recover. Hard cap, enforced even under durability-first relaxation.
|
||||
if maxPerDisk > 0 && existingShards >= maxPerDisk {
|
||||
continue
|
||||
}
|
||||
score := d.shardCount*10 + existingShards*100
|
||||
if dataShardCount > 0 {
|
||||
if isDataShard && hasParity {
|
||||
score += 1000
|
||||
} else if !isDataShard && hasData {
|
||||
score += 1000
|
||||
}
|
||||
}
|
||||
preferred := prefer == nil || prefer(d)
|
||||
if !preferred {
|
||||
score += 100000 // strongly deprioritize spilling to a non-preferred type
|
||||
}
|
||||
if bestScore == -1 || score < bestScore {
|
||||
bestScore = score
|
||||
bestDiskID = diskID
|
||||
bestPreferred = preferred
|
||||
}
|
||||
}
|
||||
if bestScore == -1 {
|
||||
return 0, false, false
|
||||
}
|
||||
return bestDiskID, true, prefer != nil && !bestPreferred
|
||||
}
|
||||
|
||||
// clearShardAccounting removes one shard copy of a volume from the snapshot's
|
||||
// per-domain accounting (the volume's shard bits) WITHOUT crediting disk capacity.
|
||||
// It clears only the given physical disk's bit, then recomputes the node-level
|
||||
// union from the remaining disk bits, so a kept copy of the same shard on another
|
||||
// disk of the same node still counts toward caps / ReplicaPlacement / anti-affinity.
|
||||
//
|
||||
// Repair uses this to drop the duplicate/mismatched copies it plans to delete
|
||||
// before placing missing shards, so those copies do not inflate placement
|
||||
// accounting. Capacity is deliberately NOT credited: the deletes run only after
|
||||
// the rebuilt shards are distributed, so the slots are not free at plan time. This
|
||||
// is distinct from releaseShard, which credits freeSlots and clears the union.
|
||||
func clearShardAccounting(node *Node, vk volKey, shardID int, diskID uint32) {
|
||||
info, ok := node.shards[vk]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
sid := erasure_coding.ShardId(shardID)
|
||||
if bits, ok := info.diskShardBits[diskID]; ok {
|
||||
info.diskShardBits[diskID] = bits.Clear(sid)
|
||||
}
|
||||
var union erasure_coding.ShardBits
|
||||
for _, b := range info.diskShardBits {
|
||||
union |= b
|
||||
}
|
||||
info.shardBits = union
|
||||
}
|
||||
|
||||
// ClearShardAccounting drops one shard copy of a volume from placement accounting
|
||||
// without crediting capacity (see clearShardAccounting). Repair calls it for each
|
||||
// copy it plans to delete before placing missing shards, so those copies do not
|
||||
// inflate caps/RP/anti-affinity. No-op for an unknown node.
|
||||
func (t *Topology) ClearShardAccounting(nodeID, collection string, vid uint32, shardID int, diskID uint32) {
|
||||
n, ok := t.nodes[nodeID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
clearShardAccounting(n, volKey{collection: collection, vid: vid}, shardID, diskID)
|
||||
}
|
||||
|
||||
// ReleaseVolumeShards removes every shard of a volume from the snapshot and
|
||||
// credits the freed disk capacity. A greenfield encode calls this so any stale
|
||||
// EC shards left by a prior failed attempt (which the encode task deletes before
|
||||
// distributing the new shards) neither occupy capacity nor skew anti-affinity /
|
||||
// per-disk caps during planning. Unlike repair's ClearShardAccounting, it credits
|
||||
// freeSlots because the deletes run before the new writes.
|
||||
func (t *Topology) ReleaseVolumeShards(collection string, vid uint32) {
|
||||
vk := volKey{collection: collection, vid: vid}
|
||||
for _, n := range t.nodes {
|
||||
info, ok := n.shards[vk]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// freed is the total disk-slots the volume occupies on this node (a shard may
|
||||
// sit on more than one disk). releaseShard credits each disk's freeSlots;
|
||||
// credit the node's freeSlots by the same total, since rack capacity is summed
|
||||
// from node freeSlots (buildRacks) and node freeSlots gates node eligibility.
|
||||
freed := 0
|
||||
for _, bits := range info.diskShardBits {
|
||||
freed += bits.Count()
|
||||
}
|
||||
sids := make([]int, 0, info.shardBits.Count())
|
||||
for sid := range info.shardBits.All() {
|
||||
sids = append(sids, int(sid))
|
||||
}
|
||||
for _, sid := range sids {
|
||||
releaseShard(n, vk, sid)
|
||||
}
|
||||
n.freeSlots += freed
|
||||
delete(n.shards, vk)
|
||||
}
|
||||
}
|
||||
|
||||
// shardsOfType returns the sorted subset of need that are data shards (id <
|
||||
// dataShards) when isData, else the parity subset.
|
||||
func shardsOfType(need []int, isData bool, dataShards int) []int {
|
||||
var out []int
|
||||
for _, s := range need {
|
||||
if (s < dataShards) == isData {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
sort.Ints(out)
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user