From 027d43b4eba19f21e6d1161605010dc43a2570c8 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Wed, 16 Sep 2026 17:09:19 +0800 Subject: [PATCH 1/4] fix: synchronize CPU resource metrics reads Hold resourceMetricsMapMu.RLock while the v3 CPU collector reads both the subsystem map and its nested idle/iowait metrics. The unsynchronized reads were inherited from upstream commit f7b665347 (minio/minio#19560). Add value and concurrent Prometheus Gather regression tests, run them in the targeted race CI job, and record the existing CPU metric names in the compatibility inventory. Fixes #210 Signed-off-by: Feng Ruohang --- .github/workflows/go.yml | 3 + .../rebrand-guard/compat-baseline.json | 8 + cmd/metrics-v3-system-cpu.go | 2 + cmd/metrics-v3-system-cpu_test.go | 185 ++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 cmd/metrics-v3-system-cpu_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0baad8fe6..7b1f7c60c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -96,6 +96,9 @@ jobs: - name: Run multipart listing and cancellation tests under race detector run: go test -race ./cmd -run '^Test(MultipartListing|MultipartAbort|PaginateMultipartUploads|ListMultipartUploads)' -count=1 -timeout=5m + - name: Run CPU metrics tests under race detector + run: go test -race ./cmd -run '^TestLoadCPUMetrics' -count=1 -timeout=5m + crosscompile: name: Cross Compile runs-on: ubuntu-latest diff --git a/buildscripts/rebrand-guard/compat-baseline.json b/buildscripts/rebrand-guard/compat-baseline.json index ff95268fd..2ff232340 100644 --- a/buildscripts/rebrand-guard/compat-baseline.json +++ b/buildscripts/rebrand-guard/compat-baseline.json @@ -571,6 +571,14 @@ "minio_resource_stats", "minio_s3", "minio_stats", + "minio_system_cpu_avg_idle", + "minio_system_cpu_avg_iowait", + "minio_system_cpu_load", + "minio_system_cpu_load_perc", + "minio_system_cpu_nice", + "minio_system_cpu_steal", + "minio_system_cpu_system", + "minio_system_cpu_user", "minio_test" ], "headers": [ diff --git a/cmd/metrics-v3-system-cpu.go b/cmd/metrics-v3-system-cpu.go index cb31b8358..dbfb802ae 100644 --- a/cmd/metrics-v3-system-cpu.go +++ b/cmd/metrics-v3-system-cpu.go @@ -69,6 +69,8 @@ func loadCPUMetrics(ctx context.Context, m MetricValues, c *metricsCache) error // metrics-resource.go runs a job to collect resource metrics including their Avg values and // stores them in resourceMetricsMap. We can use it to get the Avg values of CPU idle and IOWait. + resourceMetricsMapMu.RLock() + defer resourceMetricsMapMu.RUnlock() cpuResourceMetrics, found := resourceMetricsMap[cpuSubsystem] if found { if cpuIdleMetric, ok := cpuResourceMetrics[getResourceKey(cpuIdle, nil)]; ok { diff --git a/cmd/metrics-v3-system-cpu_test.go b/cmd/metrics-v3-system-cpu_test.go new file mode 100644 index 000000000..98ec95f57 --- /dev/null +++ b/cmd/metrics-v3-system-cpu_test.go @@ -0,0 +1,185 @@ +// Copyright (c) 2026 Ruohang Feng +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "runtime" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/cachevalue" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + cpustats "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/load" +) + +// These tests replace global resource metrics and must not run in parallel. +// Concurrent writers must finish before the fixture is restored. +func setCPUResourceMetricsForTest(t *testing.T, value map[MetricSubsystem]ResourceMetrics) { + t.Helper() + resourceMetricsMapMu.Lock() + saved := resourceMetricsMap + resourceMetricsMap = value + resourceMetricsMapMu.Unlock() + t.Cleanup(func() { + resourceMetricsMapMu.Lock() + resourceMetricsMap = saved + resourceMetricsMapMu.Unlock() + }) +} + +func newCPUMetricsTestRegistry() *prometheus.Registry { + c := &metricsCache{cpuMetrics: cachevalue.NewFromFunc(time.Hour, cachevalue.Opts{}, + func(context.Context) (madmin.CPUMetrics, error) { + return madmin.CPUMetrics{ + CPUCount: 4, + LoadStat: &load.AvgStat{Load1: 2}, + TimesStat: &cpustats.TimesStat{ + User: 10, System: 20, Idle: 60, Iowait: 5, Nice: 3, Steal: 2, + }, + }, nil + })} + _, _ = c.cpuMetrics.Get() // Warm the cache: the cache does not protect the resource map. + g := NewMetricsGroup(systemCPUCollectorPath, []MetricDescriptor{ + sysCPUAvgIdleMD, sysCPUAvgIOWaitMD, sysCPULoadMD, sysCPULoadPercMD, + sysCPUNiceMD, sysCPUStealMD, sysCPUSystemMD, sysCPUUserMD, + }, loadCPUMetrics) + g.SetCache(c) + r := prometheus.NewPedanticRegistry() + r.MustRegister(g) + return r +} + +func TestLoadCPUMetricsValues(t *testing.T) { + for _, tc := range []struct { + name string + data map[MetricSubsystem]ResourceMetrics + want map[string]float64 + }{ + {name: "nil-map"}, + {name: "empty-map", data: map[MetricSubsystem]ResourceMetrics{}}, + {name: "no-cpu", data: map[MetricSubsystem]ResourceMetrics{memSubsystem: {}}}, + {name: "nil-cpu", data: map[MetricSubsystem]ResourceMetrics{cpuSubsystem: nil}}, + {name: "empty-cpu", data: map[MetricSubsystem]ResourceMetrics{cpuSubsystem: {}}}, + {name: "idle-only", data: map[MetricSubsystem]ResourceMetrics{cpuSubsystem: { + getResourceKey(cpuIdle, nil): {Avg: 87.654}, + }}, want: map[string]float64{"minio_system_cpu_avg_idle": 87.65}}, + {name: "iowait-only", data: map[MetricSubsystem]ResourceMetrics{cpuSubsystem: { + getResourceKey(cpuIOWait, nil): {Avg: 1.236}, + }}, want: map[string]float64{"minio_system_cpu_avg_iowait": 1.24}}, + {name: "both-rounded", data: map[MetricSubsystem]ResourceMetrics{cpuSubsystem: { + getResourceKey(cpuIdle, nil): {Avg: 87.654}, + getResourceKey(cpuIOWait, nil): {Avg: 1.236}, + }}, want: map[string]float64{"minio_system_cpu_avg_idle": 87.65, "minio_system_cpu_avg_iowait": 1.24}}, + {name: "zero-keeps-existing-omission", data: map[MetricSubsystem]ResourceMetrics{cpuSubsystem: { + getResourceKey(cpuIdle, nil): {Avg: 0}, + getResourceKey(cpuIOWait, nil): {Avg: 0}, + }}}, + } { + t.Run(tc.name, func(t *testing.T) { + setCPUResourceMetricsForTest(t, tc.data) + families, err := newCPUMetricsTestRegistry().Gather() + if err != nil { + t.Fatal(err) + } + want := map[string]float64{ + "minio_system_cpu_load": 2, "minio_system_cpu_load_perc": 50, + "minio_system_cpu_nice": 3, "minio_system_cpu_steal": 2, + "minio_system_cpu_system": 20, "minio_system_cpu_user": 10, + } + for k, v := range tc.want { + want[k] = v + } + if len(families) != len(want) { + t.Fatalf("got %d families, want %d", len(families), len(want)) + } + for _, family := range families { + v, ok := want[family.GetName()] + if !ok || family.GetType() != dto.MetricType_GAUGE || len(family.Metric) != 1 || family.Metric[0].GetGauge().GetValue() != v { + t.Errorf("unexpected family: %v (want %v)", family, want) + } + } + }) + } +} + +func TestLoadCPUMetricsConcurrentUpdate(t *testing.T) { + for _, subsystem := range []MetricSubsystem{cpuSubsystem, memSubsystem} { + for _, readers := range []int{1, 4} { + t.Run(fmt.Sprintf("writer-%s/readers-%d", subsystem, readers), func(t *testing.T) { + setCPUResourceMetricsForTest(t, map[MetricSubsystem]ResourceMetrics{}) + updateResourceMetrics(cpuSubsystem, cpuIdle, 80, nil, false) + updateResourceMetrics(cpuSubsystem, cpuIOWait, 5, nil, false) + r := newCPUMetricsTestRegistry() + stop := make(chan struct{}) + done := make(chan struct{}) + ready := make(chan struct{}) + var updates atomic.Uint64 + go func() { + defer close(done) + if subsystem == cpuSubsystem { + updateResourceMetrics(subsystem, cpuIdle, 80, nil, false) + } else { + updateResourceMetrics(subsystem, memUsed, 1024, nil, false) + } + close(ready) + for { + select { + case <-stop: + return + default: + } + if subsystem == cpuSubsystem { + updateResourceMetrics(subsystem, cpuIdle, 80, nil, false) + updateResourceMetrics(subsystem, cpuIOWait, 5, nil, false) + } else { + updateResourceMetrics(subsystem, memUsed, 1024, nil, false) + } + updates.Add(1) + runtime.Gosched() + } + }() + <-ready + defer func() { close(stop); <-done }() + var wg sync.WaitGroup + for range readers { + wg.Add(1) + go func() { + defer wg.Done() + for range 1000 { + families, err := r.Gather() + if err != nil || len(families) != 8 { + t.Errorf("Gather: families=%d, error=%v", len(families), err) + return + } + } + }() + } + wg.Wait() + if updates.Load() == 0 { + t.Fatal("writer made no progress") + } + t.Logf("completed %d gathers with %d concurrent update iterations", readers*1000, updates.Load()) + }) + } + } +} From a2fe70424e697d833df07e0e07986f7103025771 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Wed, 16 Sep 2026 17:30:29 +0800 Subject: [PATCH 2/4] release: prepare SILO 20260916000000 candidate Signed-off-by: Feng Ruohang --- .github/workflows/release.yml | 4 +- .github/workflows/test-release.yml | 10 ++- CHANGELOG.md | 42 +++++++---- Dockerfile.goreleaser | 6 +- buildscripts/install-mcli.sh | 2 +- buildscripts/verify-helm-migration.sh | 2 +- cmd/untar_lz4_test.go | 101 ++++++++++++++++++++++++++ go.mod | 14 ++-- go.sum | 28 +++---- helm/silo/Chart.yaml | 4 +- helm/silo/values.yaml | 4 +- internal/s3select/lz4_test.go | 71 ++++++++++++++++++ 12 files changed, 236 insertions(+), 52 deletions(-) create mode 100644 cmd/untar_lz4_test.go create mode 100644 internal/s3select/lz4_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3350283a..ead08d4fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -132,9 +132,9 @@ jobs: cosign-release: v3.1.2 - name: Build Draft release with GoReleaser - uses: goreleaser/goreleaser-action@v7 + uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: - version: "~> v2" + version: v2.18.1 args: release --clean --skip=validate --config .github/goreleaser.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index 8408bf934..f7bf9d877 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -4,6 +4,8 @@ on: workflow_dispatch: pull_request: paths: + - "go.mod" + - "go.sum" - ".github/goreleaser.yml" - ".github/nfpm.yml" - "Dockerfile.goreleaser" @@ -93,9 +95,9 @@ jobs: echo "LDFLAGS: ${LDFLAGS}" - name: GoReleaser config check - uses: goreleaser/goreleaser-action@v7 + uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: - version: "~> v2" + version: v2.18.1 args: check --config .github/goreleaser.yml - name: Validate Helm chart and legacy upgrade identity @@ -107,9 +109,9 @@ jobs: syft-version: v1.50.0 - name: Build snapshot artifacts - uses: goreleaser/goreleaser-action@v7 + uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: - version: "~> v2" + version: v2.18.1 # A pull-request snapshot has no trusted release identity. Exercise # the SBOM/checksum pipeline here, and reserve keyless signing for # the tag-triggered release workflow with GitHub OIDC. diff --git a/CHANGELOG.md b/CHANGELOG.md index a0468f138..13ea35fa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,18 @@ ## Unreleased -The entries below describe source changes on main since the latest published Server. +Preparation target: `RELEASE.2026-09-16T00-00-00Z` (package version +`20260916000000.0.0`). The entries below describe the candidate changes since +the latest published Server. **The latest published Server remains 20260903.** These changes are not in its binaries, packages or images. See the [component matrix](https://silo.pgsty.com/compatibility/versions/) and [complete commit range](https://github.com/pgsty/silo/compare/RELEASE.2026-09-03T13-18-01Z...main). ### Authorization and security +- Synchronize CPU metrics reads with resource-metrics updates (#210), preventing + concurrent map access from terminating the server during Prometheus scraping. + Metric names, values and authentication requirements are unchanged. - Restrict embedded Console's anonymous sharing proxy to object-content GETs at the configured S3 origin, and reject every redirect. Internal metrics, system paths and non-download S3 operations cannot be reached through it. @@ -167,23 +172,28 @@ and [complete commit range](https://github.com/pgsty/silo/compare/RELEASE.2026-0 - Restore embedded Console login over loopback TLS, trusted-proxy handling and all four WebSocket connection limits. Preserve Go TLS defaults across transports. -- Directly require `github.com/pgsty/silo-pkg/v3` v3.14.0; select Console - `v0.0.0-20260916034812-56dfe455ac2f` and MC - `v0.0.0-20260913012246-4f609a4da3bb` with explicit PGSTY replacements. -- Pin upstream minio-go `v7.3.1-0.20260910142817-60bd07042d49`; refresh Go x/* - modules and security fixes including bounded AMQP frame handling. Keep Go - 1.27.1 and go-systemd v22.6.0's NetBSD compatibility replacement. +- Directly require `github.com/pgsty/silo-pkg/v3` v3.14.1; select released Console + v2.4.1 (`v0.0.0-20260916075814-1360e26d976d`) and mcli 20260916 + (`v0.0.0-20260916070421-e952aa78f10a`) with explicit PGSTY replacements. + The embedded frontend identifies itself as Console v2.4.1. +- Pin upstream minio-go `v7.3.1-0.20260915093545-32e1f32cb176` to handle + CopyObject errors embedded in HTTP 200 responses. Update JWX to v3.3.0 for + JSON field-name escaping, strfmt to v0.27.2 for Go 1.27 hostname validation, + and LZ4 to v4.1.30 for frame-reader, partial-read and concurrency fixes. + Retain the earlier Go x/* and bounded AMQP frame updates, Go 1.27.1, and + go-systemd v22.6.0's NetBSD compatibility replacement. - Refresh container base digests and build static curl 8.22.0 from verified - source for both Linux architectures. Pin the actual mcli 20260913 archives and - hashes. Helm's client image follows that release; its Server image still names - the latest published Server 20260903. + source for both Linux architectures. Pin the published mcli 20260916 archives + and hashes in the container and update the client installer default. +- Prepare Helm chart 7.0.3 with Server and client defaults for the September 16 + batch. Publish the chart only after the corresponding Server image exists. +- Pin GoReleaser v2.18.1 and its action commit identically in snapshot and release + workflows. Dependency-only PRs now run the Test Release Pipeline too. -The dependency update passed the final candidate's Go, vulnerability and Test -Release workflows; native curl builds passed on both architectures. A local -ARM64 image passed startup, health, S3 transfer and embedded Console checks. -These checks do not publish a Server tag or production image and do not replace -cluster upgrade/rollback acceptance for the next release. Dated investigations -retain the exact source and runtime boundaries they tested. +Validation of earlier source revisions does not establish acceptance of this +candidate. Final source, package, image and multi-process checks are tracked +separately in [#203](https://github.com/pgsty/silo/issues/203). No Server release +or production rollout is implied by this preparation target. ## RELEASE.2026-09-03T13-18-01Z diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index 4d9d4b300..5311ef438 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -17,9 +17,9 @@ ENV GOPATH=/go ENV CGO_ENABLED=0 ARG MC_REPO=pgsty/mc -ARG MC_VERSION=RELEASE.2026-09-13T00-00-00Z -ARG MC_AMD64_SHA256=9d2a92de9c7b887d9b944fe9ddce68d23f1b6df3415092e737594e56593f5e2b -ARG MC_ARM64_SHA256=3d82e9ea6c601c4cb44fe5dd5f2ad1b7d7d64369378110f9ada9c524688a452a +ARG MC_VERSION=RELEASE.2026-09-16T00-00-00Z +ARG MC_AMD64_SHA256=4ba2814fd5507fbe6b4d237c359750b9119d28d7217495fa5b48002fcbd397ef +ARG MC_ARM64_SHA256=b7008ca2a1bc5735b6585981c59a3640a0daa152dc789df3d1a0d0438787de82 RUN apk add -U --no-cache \ ca-certificates \ diff --git a/buildscripts/install-mcli.sh b/buildscripts/install-mcli.sh index bd4c9c6ed..646b8416a 100755 --- a/buildscripts/install-mcli.sh +++ b/buildscripts/install-mcli.sh @@ -40,7 +40,7 @@ if [ -n "${MCLI_BIN:-}" ]; then exit 0 fi -release=${MCLI_RELEASE:-RELEASE.2026-09-13T00-00-00Z} +release=${MCLI_RELEASE:-RELEASE.2026-09-16T00-00-00Z} version_hyphen=${release#RELEASE.} package_version=$(printf '%s\n' "${version_hyphen}" | sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/') if [ "${package_version}" = "${version_hyphen}" ]; then diff --git a/buildscripts/verify-helm-migration.sh b/buildscripts/verify-helm-migration.sh index de33723c9..138b1ef03 100755 --- a/buildscripts/verify-helm-migration.sh +++ b/buildscripts/verify-helm-migration.sh @@ -113,7 +113,7 @@ helm_run template my-release "${new_chart}" \ go run ./buildscripts/helm-migration-guard "${old_render}" "${new_render}" helm_run package "${new_chart}" --destination "${output_dir}" >/dev/null -test -s "${work_dir}/silo-7.0.2.tgz" +test -s "${work_dir}/silo-7.0.3.tgz" if find "${work_dir}" -maxdepth 1 -type f -name 'minio-*.tgz' | grep -q .; then echo "Helm packaging emitted a legacy MinIO chart name" >&2 exit 1 diff --git a/cmd/untar_lz4_test.go b/cmd/untar_lz4_test.go new file mode 100644 index 000000000..6e5b2b557 --- /dev/null +++ b/cmd/untar_lz4_test.go @@ -0,0 +1,101 @@ +// Copyright (c) 2026 Feng Ruohang +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "os" + "sync" + "testing" + "testing/iotest" + + "github.com/pierrec/lz4/v4" +) + +func TestUntarLZ4(t *testing.T) { + for _, empty := range []bool{false, true} { + t.Run(fmt.Sprintf("empty=%t", empty), func(t *testing.T) { + want := map[string][]byte{} + if !empty { + want["small.txt"] = bytes.Repeat([]byte("small object\n"), 10) + want["large.txt"] = bytes.Repeat([]byte("large object\n"), 32768) + } + var compressed bytes.Buffer + compressor := lz4.NewWriter(&compressed) + archive := tar.NewWriter(compressor) + for name, data := range want { + if err := archive.WriteHeader(&tar.Header{Name: name, Mode: 0600, Size: int64(len(data))}); err != nil { + t.Fatal(err) + } + if _, err := archive.Write(data); err != nil { + t.Fatal(err) + } + } + if err := archive.Close(); err != nil { + t.Fatal(err) + } + if err := compressor.Close(); err != nil { + t.Fatal(err) + } + var mu sync.Mutex + got := map[string][]byte{} + err := untar(t.Context(), iotest.HalfReader(bytes.NewReader(compressed.Bytes())), func(r io.Reader, info os.FileInfo, name string) error { + // Exercise a short first read followed by streaming the remaining content. + var output bytes.Buffer + prefix := make([]byte, 7) + if _, err := io.ReadFull(r, prefix); err != nil { + return err + } + output.Write(prefix) + if _, err := io.Copy(&output, r); err != nil { + return err + } + if int64(output.Len()) != info.Size() { + return fmt.Errorf("size mismatch for %s", name) + } + mu.Lock() + got[name] = output.Bytes() + mu.Unlock() + return nil + }, untarOptions{}) + if err != nil { + t.Fatal(err) + } + if len(got) != len(want) { + t.Fatalf("objects=%d, want %d", len(got), len(want)) + } + for name, data := range want { + if !bytes.Equal(got[name], data) { + t.Errorf("content mismatch for %s", name) + } + } + t.Run("corrupt-header", func(t *testing.T) { + damaged := bytes.Clone(compressed.Bytes()) + damaged[6] ^= 0xff + err := untar(t.Context(), bytes.NewReader(damaged), func(io.Reader, os.FileInfo, string) error { + t.Error("corrupt header must be rejected before uploading objects") + return nil + }, untarOptions{}) + if err == nil { + t.Fatal("corrupt LZ4 header accepted") + } + }) + }) + } +} diff --git a/go.mod b/go.mod index b3a9f2a94..ebe162bac 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.27.1 // Console and MC retain their historical module paths for best-effort upstream // compatibility. Pin the maintained PGSTY implementations used by SILO. -replace github.com/minio/console => github.com/pgsty/silo-console v0.0.0-20260916034812-56dfe455ac2f +replace github.com/minio/console => github.com/pgsty/silo-console v0.0.0-20260916075814-1360e26d976d -replace github.com/minio/mc => github.com/pgsty/mc v0.0.0-20260913012246-4f609a4da3bb +replace github.com/minio/mc => github.com/pgsty/mc v0.0.0-20260916070421-e952aa78f10a // v22.7.0 does not compile on NetBSD because its unix implementation uses // CLOCK_MONOTONIC, which is unavailable there. Keep the last portable release @@ -68,7 +68,7 @@ require ( github.com/minio/kms-go/kes v0.3.1 github.com/minio/kms-go/kms v0.6.0 github.com/minio/madmin-go/v3 v3.0.110 - github.com/minio/minio-go/v7 v7.3.1-0.20260910142817-60bd07042d49 + github.com/minio/minio-go/v7 v7.3.1-0.20260915093545-32e1f32cb176 github.com/minio/mux v1.10.1 github.com/minio/selfupdate v0.6.0 github.com/minio/simdjson-go v0.4.5 @@ -81,9 +81,9 @@ require ( github.com/nats-io/stan.go v0.10.4 github.com/ncw/directio v1.0.5 github.com/nsqio/go-nsq v1.1.0 - github.com/pgsty/silo-pkg/v3 v3.14.0 + github.com/pgsty/silo-pkg/v3 v3.14.1 github.com/philhofer/fwd v1.2.0 - github.com/pierrec/lz4/v4 v4.1.29 + github.com/pierrec/lz4/v4 v4.1.30 github.com/pkg/errors v0.9.1 github.com/pkg/sftp v1.13.11 github.com/pkg/xattr v0.4.12 @@ -175,7 +175,7 @@ require ( github.com/go-openapi/runtime v0.33.1 // indirect github.com/go-openapi/runtime/server-middleware v0.33.1 // indirect github.com/go-openapi/spec v1.0.0 // indirect - github.com/go-openapi/strfmt v0.27.0 // indirect + github.com/go-openapi/strfmt v0.27.2 // indirect github.com/go-openapi/swag v0.29.1 // indirect github.com/go-openapi/swag/cmdutils v0.29.1 // indirect github.com/go-openapi/swag/conv v0.29.1 // indirect @@ -225,7 +225,7 @@ require ( github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc/v3 v3.0.6 // indirect - github.com/lestrrat-go/jwx/v3 v3.2.0 // indirect + github.com/lestrrat-go/jwx/v3 v3.3.0 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.1 // indirect github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect diff --git a/go.sum b/go.sum index 9c6aa119a..a08f49d24 100644 --- a/go.sum +++ b/go.sum @@ -214,8 +214,8 @@ github.com/go-openapi/runtime/server-middleware v0.33.1 h1:IAeKbwWnBnpsYTpuPVS8t github.com/go-openapi/runtime/server-middleware v0.33.1/go.mod h1:2Gej5fDxqeJxY+w38vxXYW0BgFASfgBsJ5rXwN1Fseg= github.com/go-openapi/spec v1.0.0 h1:JtB/GHOj+eetjse6YvxqLze88oEekl/4uPBethvzRrA= github.com/go-openapi/spec v1.0.0/go.mod h1:boj1PRhqS0x5jylgcNp9BRWxenphrh7vVNYZ90IRCoo= -github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM= -github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc= +github.com/go-openapi/strfmt v0.27.2 h1:SG32SlbwNy92s0KJiVxt2joJeFdqIYHvwrA0OU6HqzQ= +github.com/go-openapi/strfmt v0.27.2/go.mod h1:M4CKsMO0Fb8qR10+1Ra75wCKNNquy+Vj+4LWZrhTo2E= github.com/go-openapi/swag v0.29.1 h1:C6EeWzUwQtcWEhE9eqBdUubGXxhWY4PlzHMLD7kLaiQ= github.com/go-openapi/swag v0.29.1/go.mod h1:BzxEXKiPlSXRsRTv1KSBF/BpGKHxA/YciCnr4tv9bvA= github.com/go-openapi/swag/cmdutils v0.29.1 h1:3DorPGfUdE80BogKY22EzoHBcHMrkVomZMoV7kS4ANY= @@ -411,8 +411,8 @@ github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZ github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httprc/v3 v3.0.6 h1:4FpLQ18KK/ypPbVU3NLWJNRvH3kcYiqKqWfKGqNWxxI= github.com/lestrrat-go/httprc/v3 v3.0.6/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= -github.com/lestrrat-go/jwx/v3 v3.2.0 h1:Jb3zBASTSZXz7gzzSAfYqxXF8KejvKC4xWoePLQqXCA= -github.com/lestrrat-go/jwx/v3 v3.2.0/go.mod h1:38vQ8iWKq3qRSbilbzvzdQPuywhowwuR03lhkYskyrw= +github.com/lestrrat-go/jwx/v3 v3.3.0 h1:OXcYvQOQ7cxWzeZ/Q9sYk8ABe/kCSI371WmuACiCT+4= +github.com/lestrrat-go/jwx/v3 v3.3.0/go.mod h1:eIJhDcKHBwcgxqv8RiIylV67TVl1wJp/265IAHY1Db8= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -474,8 +474,8 @@ github.com/minio/madmin-go/v3 v3.0.110 h1:FIYekj7YPc430ffpXFWiUtyut3qBt/unIAcDzJ github.com/minio/madmin-go/v3 v3.0.110/go.mod h1:WOe2kYmYl1OIlY2DSRHVQ8j1v4OItARQ6jGyQqcCud8= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.3.1-0.20260910142817-60bd07042d49 h1:xDONRymps7J67VLaNzMqq25qYCHHm3JgXnKPRf1NoFs= -github.com/minio/minio-go/v7 v7.3.1-0.20260910142817-60bd07042d49/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk= +github.com/minio/minio-go/v7 v7.3.1-0.20260915093545-32e1f32cb176 h1:TIrbAkJoMN/kbtV4LAitf/eidrbvvYcujXPbSe+4j/0= +github.com/minio/minio-go/v7 v7.3.1-0.20260915093545-32e1f32cb176/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk= github.com/minio/mux v1.10.1 h1:grrK8SwRKbkNFE6qG7WAvFGH09bB46d5teOOtKfQ14s= github.com/minio/mux v1.10.1/go.mod h1:INYT4sMSTJy0QWUEA/E2DZNxJ5sAxIwbnyZjkzNFRfE= github.com/minio/pkg/v3 v3.6.1 h1:gaNT80BS/iuIany5ylTkVmfN4s6UYY30OtImFv4GQA8= @@ -545,16 +545,16 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzb github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pgsty/mc v0.0.0-20260913012246-4f609a4da3bb h1:S7IAYBoKvFRqrw5MUsQE34/q4cKC4K7gUmnlEGxWtqo= -github.com/pgsty/mc v0.0.0-20260913012246-4f609a4da3bb/go.mod h1:kJN7dsWtSUhXd2vNPXdjDi/or6lJKlSLfb56cBfMckA= -github.com/pgsty/silo-console v0.0.0-20260916034812-56dfe455ac2f h1:ull9m/nXOEMfggKtMQP2idYXPh6l6chCUJ9hJHYyzDc= -github.com/pgsty/silo-console v0.0.0-20260916034812-56dfe455ac2f/go.mod h1:YtRQZ6jYXRUE03oPA+IMGtflQN6nCvDWdKtroA7tfKo= -github.com/pgsty/silo-pkg/v3 v3.14.0 h1:RCuVkzr6mdjbkV/Rv4OVO/XgdMBE0XYvUnT6GFuihFk= -github.com/pgsty/silo-pkg/v3 v3.14.0/go.mod h1:c26IoMVITlP1+Sirl0AsHwoijlsSC9wPpyCuvhb3Axc= +github.com/pgsty/mc v0.0.0-20260916070421-e952aa78f10a h1:FY1vcTQT67HGBe23ozlhOiQnWCzUCGmeSN5x6DT/kFs= +github.com/pgsty/mc v0.0.0-20260916070421-e952aa78f10a/go.mod h1:6z9lX8kOvaWz2vLlP7BjjF3JWHnIH86XDA2HwcmpZH8= +github.com/pgsty/silo-console v0.0.0-20260916075814-1360e26d976d h1:A9ou1GkUckGZd3BOgEXeZ393nbYST967SKcI4Lgr/Vs= +github.com/pgsty/silo-console v0.0.0-20260916075814-1360e26d976d/go.mod h1:JS165CF2yoTBIx1X8k1IQ6vunJcZFG3pRZlWHJ8UT8o= +github.com/pgsty/silo-pkg/v3 v3.14.1 h1:8MONT3Hky9EOnsuPk29590pEBE6MILaOnJtaejPPi7M= +github.com/pgsty/silo-pkg/v3 v3.14.1/go.mod h1:jxKWxi52jSsDaDhXXPqjOyPDYdxx2iqdBXyLfM/xSIw= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pierrec/lz4/v4 v4.1.29 h1:CDQY6qZOLI4DW0Nx6R1vRrifrCeQHnNXkMb0hZWXFjg= -github.com/pierrec/lz4/v4 v4.1.29/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pierrec/lz4/v4 v4.1.30 h1:cchX8N2DVP668WkElI9QMwVyoNabLkq1LofDHFeIrdg= +github.com/pierrec/lz4/v4 v4.1.30/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/helm/silo/Chart.yaml b/helm/silo/Chart.yaml index ba3123e00..ad8cdebc2 100644 --- a/helm/silo/Chart.yaml +++ b/helm/silo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: S3-Interface Libre Object Storage name: silo -version: 7.0.2 -appVersion: RELEASE.2026-09-03T13-18-01Z +version: 7.0.3 +appVersion: RELEASE.2026-09-16T00-00-00Z keywords: - silo - storage diff --git a/helm/silo/values.yaml b/helm/silo/values.yaml index 994765c91..400586cea 100644 --- a/helm/silo/values.yaml +++ b/helm/silo/values.yaml @@ -15,7 +15,7 @@ clusterDomain: cluster.local ## image: repository: pgsty/silo - tag: RELEASE.2026-09-03T13-18-01Z + tag: RELEASE.2026-09-16T00-00-00Z pullPolicy: IfNotPresent imagePullSecrets: [] @@ -25,7 +25,7 @@ imagePullSecrets: [] ## mcImage: repository: pgsty/mc - tag: RELEASE.2026-09-13T00-00-00Z + tag: RELEASE.2026-09-16T00-00-00Z pullPolicy: IfNotPresent ## Silo mode, i.e. standalone or distributed. diff --git a/internal/s3select/lz4_test.go b/internal/s3select/lz4_test.go new file mode 100644 index 000000000..3428a756d --- /dev/null +++ b/internal/s3select/lz4_test.go @@ -0,0 +1,71 @@ +// Copyright (c) 2026 Feng Ruohang +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package s3select + +import ( + "bytes" + "io" + "testing" + "testing/iotest" + + "github.com/pierrec/lz4/v4" +) + +func TestLZ4Input(t *testing.T) { + request := []byte(` +SELECT * FROM S3ObjectSQL +LZ4NONE +`) + for _, input := range []string{"", "one,1\ntwo,2\n"} { + t.Run(input, func(t *testing.T) { + var compressed bytes.Buffer + writer := lz4.NewWriter(&compressed) + if _, err := io.WriteString(writer, input); err != nil { + t.Fatal(err) + } + if err := writer.Close(); err != nil { + t.Fatal(err) + } + got, err := evaluateSelectForTest(t, request, compressed.Bytes()) + if err != nil { + t.Fatal(err) + } + if string(got) != input { + t.Fatalf("result=%q, want %q", got, input) + } + + reader, err := newProgressReader(io.NopCloser(iotest.OneByteReader(bytes.NewReader(compressed.Bytes()))), lz4Type) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = reader.Close() }) + got, err = io.ReadAll(reader) + if err != nil || string(got) != input { + t.Fatalf("fragmented input: result=%q, err=%v", got, err) + } + scanned, processed := reader.Stats() + if scanned != int64(compressed.Len()) || processed != int64(len(input)) { + t.Fatalf("scanned=%d processed=%d", scanned, processed) + } + if err := reader.Close(); err != nil { + t.Fatal(err) + } + if _, err := reader.Read(make([]byte, 1)); err == nil { + t.Fatal("read after Close succeeded") + } + }) + } +} From 37c0edc7caea7b3c079bbcdc93ae69ba98f24f02 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Wed, 16 Sep 2026 17:33:58 +0800 Subject: [PATCH 3/4] test: use canonical octal permissions in LZ4 fixture Signed-off-by: Feng Ruohang --- cmd/untar_lz4_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/untar_lz4_test.go b/cmd/untar_lz4_test.go index 6e5b2b557..b073315ee 100644 --- a/cmd/untar_lz4_test.go +++ b/cmd/untar_lz4_test.go @@ -40,7 +40,7 @@ func TestUntarLZ4(t *testing.T) { compressor := lz4.NewWriter(&compressed) archive := tar.NewWriter(compressor) for name, data := range want { - if err := archive.WriteHeader(&tar.Header{Name: name, Mode: 0600, Size: int64(len(data))}); err != nil { + if err := archive.WriteHeader(&tar.Header{Name: name, Mode: 0o600, Size: int64(len(data))}); err != nil { t.Fatal(err) } if _, err := archive.Write(data); err != nil { From e7963381baf11a69c0df1ede01c38026a96b7e36 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Wed, 16 Sep 2026 17:37:47 +0800 Subject: [PATCH 4/4] test: synchronize tag replication capacity fixtures Signed-off-by: Feng Ruohang --- .github/workflows/go.yml | 3 ++ cmd/replication-tagging-order_test.go | 44 ++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 7b1f7c60c..860eebf83 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -99,6 +99,9 @@ jobs: - name: Run CPU metrics tests under race detector run: go test -race ./cmd -run '^TestLoadCPUMetrics' -count=1 -timeout=5m + - name: Run tag replication tests under race detector + run: go test -race ./cmd -run '^TestAPITagging' -count=1 -timeout=5m + crosscompile: name: Cross Compile runs-on: ubuntu-latest diff --git a/cmd/replication-tagging-order_test.go b/cmd/replication-tagging-order_test.go index 7baf3b7bf..2aa9f534c 100644 --- a/cmd/replication-tagging-order_test.go +++ b/cmd/replication-tagging-order_test.go @@ -41,15 +41,23 @@ const r5TagStamp = ReservedMetadataPrefixLower + TaggingTimestamp func r5Capacity(z *erasureServerPools) func() { var restores []func() for _, pool := range z.serverPools { + pool.erasureDisksMu.Lock() for _, set := range pool.sets { - old := set.getDisks - disks := append([]StorageAPI(nil), old()...) + old := pool.erasureDisks[set.setIndex] + disks := append([]StorageAPI(nil), old...) for i := range disks { disks[i] = tagTestCapacityDisk{StorageAPI: disks[i]} } - set.getDisks = func() []StorageAPI { return disks } - restores = append(restores, func() { set.getDisks = old }) + // GetDisks copies this list under the same mutex. Keep its function + // stable while background IAM scans are using the fixture. + pool.erasureDisks[set.setIndex] = disks + restores = append(restores, func() { + pool.erasureDisksMu.Lock() + pool.erasureDisks[set.setIndex] = old + pool.erasureDisksMu.Unlock() + }) } + pool.erasureDisksMu.Unlock() } return func() { for _, restore := range restores { @@ -58,6 +66,34 @@ func r5Capacity(z *erasureServerPools) func() { } } +func TestAPITaggingCapacityConcurrentIAM(t *testing.T) { + z, _ := consistencyPools(t) + if _, _, err := initAPIHandlerTest(t.Context(), z, nil, MakeBucketOptions{}); err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) + defer cancel() + iam := globalIAMSys + started, finished := make(chan struct{}), make(chan error, 1) + go func() { + close(started) + for range 20 { + if err := iam.Load(ctx, false); err != nil { + finished <- err + return + } + } + finished <- nil + }() + <-started + for range 5000 { + r5Capacity(z)() + } + if err := <-finished; err != nil { + t.Fatal(err) + } +} + func r5Request(t *testing.T, router http.Handler, cred auth.Credentials, method, path, body string, headers map[string]string) *httptest.ResponseRecorder { t.Helper() r, err := newTestSignedRequestV4(method, path, int64(len(body)), strings.NewReader(body), cred.AccessKey, cred.SecretKey, headers)