From bd24b88628816db8e575e33028d2e70d7ef0a5ad Mon Sep 17 00:00:00 2001 From: alexsavio Date: Mon, 10 Aug 2026 12:59:37 +0200 Subject: [PATCH 1/4] feat: write runtime crash output to a file in daemon mode (#7) After a successful daemon-mode mount the process closes stdout/stderr, so Go runtime panics and fatal errors vanish without a trace; syslog only carries logger output. Arm debug.SetCrashOutput with an append-only file (default /var/log/geesefs-crash.log, GEESEFS_CRASH_LOG to override) before closing stderr, with a pid/bucket/mountpoint header line so a following dump can be attributed to its mount. --- main.go | 4 ++++ main_nowindows.go | 30 ++++++++++++++++++++++++++++++ main_windows.go | 5 +++++ 3 files changed, 39 insertions(+) diff --git a/main.go b/main.go index bebda3ab..670ad9d7 100644 --- a/main.go +++ b/main.go @@ -152,6 +152,10 @@ func main() { log.Println("File system has been successfully mounted.") if !flags.Foreground { daemonizer.NotifySuccess(true) + // Runtime panics write to fd 2, which the two Close calls + // below discard, so a crashed daemon leaves no trace anywhere. + // Route crash output to a file first (Go keeps a dup of it). + setupCrashLog(bucketName, flags.MountPoint) os.Stderr.Close() os.Stdout.Close() } diff --git a/main_nowindows.go b/main_nowindows.go index 22dcff1e..96890b0c 100644 --- a/main_nowindows.go +++ b/main_nowindows.go @@ -22,9 +22,11 @@ import ( "fmt" "os" "os/signal" + "runtime/debug" "strings" "sync" "syscall" + "time" daemon "github.com/sevlyar/go-daemon" @@ -32,6 +34,34 @@ import ( "github.com/yandex-cloud/geesefs/core/cfg" ) +// Where Go runtime crash output goes once the daemon has closed its stderr. +// Overridable for non-root mounts via GEESEFS_CRASH_LOG. +const defaultCrashLogPath = "/var/log/geesefs-crash.log" + +// setupCrashLog routes runtime crash output (panics, fatal errors, deadlock +// dumps) to a shared append-only file. These bypass the logger and write to +// fd 2, which the daemon closes right after mounting, so without this a +// crashed daemon leaves no trace anywhere. The header line ties the pid to +// its mount so a following dump can be attributed. +func setupCrashLog(bucketName string, mountPoint string) { + path := os.Getenv("GEESEFS_CRASH_LOG") + if path == "" { + path = defaultCrashLogPath + } + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) + if err != nil { + log.Warnf("Cannot open crash log %v: %v; runtime panics will be lost", path, err) + return + } + fmt.Fprintf(f, "--- %s geesefs pid %d serving %s at %s: crash output armed ---\n", + time.Now().UTC().Format(time.RFC3339), os.Getpid(), bucketName, mountPoint) + if err = debug.SetCrashOutput(f, debug.CrashOptions{}); err != nil { + log.Warnf("Cannot set crash output to %v: %v", path, err) + } + // SetCrashOutput keeps its own duplicate of the descriptor. + f.Close() +} + var signalsToHandle = []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGUSR1} func isSigUsr1(s os.Signal) bool { diff --git a/main_windows.go b/main_windows.go index afbdf14d..d6a3c219 100644 --- a/main_windows.go +++ b/main_windows.go @@ -25,6 +25,11 @@ import ( var signalsToHandle = []os.Signal{os.Interrupt, syscall.SIGTERM} +// Unreachable on Windows: canDaemonize is false, so the daemon-only branch +// that calls this never runs. Present only to keep the build compiling. +func setupCrashLog(bucketName string, mountPoint string) { +} + func isSigUsr1(s os.Signal) bool { return false } From acd521730f69a7d30b784827792ae46f38d3edf9 Mon Sep 17 00:00:00 2001 From: Alexandre Manhaes Savio Date: Mon, 10 Aug 2026 13:03:00 +0200 Subject: [PATCH 2/4] chore: bump version to 0.43.8-dc.2 Picks up the daemon-mode crash-log feature from #7, which was merged into master rather than dev, so it was absent from the release line. Merges master into dev the same way #5 did, then bumps the version. Also refreshes the release workflow's dispatch default, which otherwise still offers the previous version. --- .github/workflows/dectris-release.yml | 4 ++-- core/cfg/flags.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dectris-release.yml b/.github/workflows/dectris-release.yml index 5146b0fa..919829eb 100644 --- a/.github/workflows/dectris-release.yml +++ b/.github/workflows/dectris-release.yml @@ -8,10 +8,10 @@ on: workflow_dispatch: inputs: version: - description: "Release version (e.g. 0.43.8-dc.1). A 'v' prefix will be added for the tag/release." + description: "Release version (e.g. 0.43.8-dc.2). A 'v' prefix will be added for the tag/release." required: true type: string - default: '0.43.8-dc.1' + default: '0.43.8-dc.2' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/core/cfg/flags.go b/core/cfg/flags.go index da32db04..944e227e 100644 --- a/core/cfg/flags.go +++ b/core/cfg/flags.go @@ -30,7 +30,7 @@ import ( "github.com/urfave/cli" ) -const GEESEFS_VERSION = "0.43.8-dc.1" +const GEESEFS_VERSION = "0.43.8-dc.2" var flagCategories map[string]string From 7ea0be5277bf2278607482963632be3ec00111df Mon Sep 17 00:00:00 2001 From: Alexandre Manhaes Savio Date: Mon, 10 Aug 2026 13:45:36 +0200 Subject: [PATCH 3/4] docs: document the release process, drop the stale version default The release version is a dispatch parameter, so editing the workflow to change it is unnecessary churn. That is also how the default drifted: it sat at 0.43.0-dc.1 while the shipped constant had moved on. Removes the default outright rather than bumping it. It duplicated GEESEFS_VERSION and went stale the moment that constant moved; the input is already required, so dispatch prompts for it. Documents in CLAUDE.md the branch model (dev is the release line, master mirrors upstream, fork work on master is missing from releases), the release commands, and the compute-amis pin that a new release has to follow. --- .github/workflows/dectris-release.yml | 6 ++++-- CLAUDE.md | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dectris-release.yml b/.github/workflows/dectris-release.yml index 919829eb..cb1d0808 100644 --- a/.github/workflows/dectris-release.yml +++ b/.github/workflows/dectris-release.yml @@ -8,10 +8,12 @@ on: workflow_dispatch: inputs: version: - description: "Release version (e.g. 0.43.8-dc.2). A 'v' prefix will be added for the tag/release." + # Deliberately has no default: it must match GEESEFS_VERSION in + # core/cfg/flags.go, and a default here goes stale the moment that + # constant moves. Pass it explicitly at dispatch time. + description: "Release version, must match GEESEFS_VERSION in core/cfg/flags.go (e.g. 0.43.8-dc.2). A 'v' prefix is added for the tag/release." required: true type: string - default: '0.43.8-dc.2' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/CLAUDE.md b/CLAUDE.md index b60714eb..ade20c47 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,6 +80,32 @@ cd docker && just test-multipart-boundary **Virtual symlinks**: Symlinks stored in `.geesefs_symlinks` (no S3 object) are distinguished from S3-backed symlinks via `Inode.isVirtualSymlink` bool field. Always use this field for detection, not `userMetadata[SymlinkAttr] != nil`. +## Branches + +- `dev` — the dectris release line. All fork work lands here, and releases are cut from it. +- `master` — mirror of `yandex-cloud/geesefs`. Sync upstream here, then merge `master` into `dev`. + +Landing fork work on `master` means it is missing from `dev`, and a release cut afterwards silently ships without it. + +## Releasing + +The version lives in one place: `GEESEFS_VERSION` in `core/cfg/flags.go`. Bump it, merge to `dev`, then dispatch: + +```bash +gh workflow run dectris-release.yml -f version= --ref dev +gh release edit v --prerelease=false --latest +``` + +The version is a dispatch parameter, so do not edit the workflow to change it. It has no default on purpose: a default duplicates `GEESEFS_VERSION` and goes stale the moment that constant moves. + +A dispatched run is marked prerelease, hence the `release edit`. Pushing a `v*` tag instead publishes a full release directly, but only the dispatch path is exercised regularly. + +Tag scheme: `v-dc.`, e.g. `v0.43.8-dc.2`. + ## Go Module Module: `github.com/yandex-cloud/geesefs` (Go 1.25). Uses the stock `github.com/aws/aws-sdk-go` with no `replace` directive. Yandex-only S3 extensions (`PatchObject`, `ListObjectsV1Ext`) live in `core/ycs3ext/`, built on the SDK's `request.Request` rather than a forked SDK. + +## Downstream + +`compute-amis` pins the version in `terraform/infrastructure/main.tf` (`geesefs_version`) and its AMI recipe downloads the release asset. A new release needs a matching bump there. From a05c60d26f966fd9086baa757e0a433130893e3f Mon Sep 17 00:00:00 2001 From: Alexandre Manhaes Savio Date: Mon, 10 Aug 2026 14:08:04 +0200 Subject: [PATCH 4/4] docs: state the branch model explicitly in CLAUDE.md Makes dev the documented trunk for all dectris development, and master a read-only mirror that must always sit on the exact upstream commit the fork is based on, with the command to verify that invariant. Spells out the failure mode behind #7: a PR against master leaves its content absent from dev, and a release cut afterwards ships without it while nothing fails. Records the recovery, and notes that the pre-rebase branches still on the remote are superseded. --- CLAUDE.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ade20c47..2aee87b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,10 +82,20 @@ cd docker && just test-multipart-boundary ## Branches -- `dev` — the dectris release line. All fork work lands here, and releases are cut from it. -- `master` — mirror of `yandex-cloud/geesefs`. Sync upstream here, then merge `master` into `dev`. +**`dev` is the main branch.** All dectris development on geesefs lives here, every PR targets it, and releases are cut from it. Treat it as the trunk. -Landing fork work on `master` means it is missing from `dev`, and a release cut afterwards silently ships without it. +**`master` is a read-only mirror of `yandex-cloud/geesefs`.** It must always point at the exact upstream commit the fork is based on, and must never carry a dectris change. Verify with: + +```bash +git fetch upstream +git rev-list --count upstream/master..origin/master # must be 0 +``` + +Upstream syncs go `master` → `dev`: fast-forward `master` to the new upstream commit, then merge `master` into `dev` and resolve there. + +Opening a PR against `master` is always wrong. Its content will be absent from `dev`, and a release cut afterwards silently ships without it, with nothing failing to warn you. If it happens, merge `master` into `dev` to recover the work, then reset `master` back to the upstream commit. + +Older branches on the remote (`symlinks`, `fix/utf-8`, `ci/dev_releases`, `dectris-master`, `perf-fio-sync-master`, …) predate this layout and are superseded. Their content is already in `dev`; do not branch from them. ## Releasing