{"id":36655639,"url":"https://github.com/moeryomenko/squad","last_synced_at":"2026-01-12T10:18:04.409Z","repository":{"id":39092373,"uuid":"373187916","full_name":"moeryomenko/squad","owner":"moeryomenko","description":"Squad is package contains a shared shutdown primitive with helpful options.","archived":false,"fork":false,"pushed_at":"2025-11-26T07:23:53.000Z","size":196,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-29T06:48:11.189Z","etag":null,"topics":["errgroup","golang","graceful-shutdown"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/moeryomenko/squad","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/moeryomenko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-06-02T13:59:56.000Z","updated_at":"2025-11-26T07:23:57.000Z","dependencies_parsed_at":"2024-08-06T13:27:54.666Z","dependency_job_id":"fecaa944-ec28-4e34-9f75-3c680cebf1a2","html_url":"https://github.com/moeryomenko/squad","commit_stats":{"total_commits":27,"total_committers":1,"mean_commits":27.0,"dds":0.0,"last_synced_commit":"51c09ccb987cd310ff52e2fb24534b1ca009189d"},"previous_names":[],"tags_count":26,"template":false,"template_full_name":null,"purl":"pkg:github/moeryomenko/squad","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moeryomenko%2Fsquad","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moeryomenko%2Fsquad/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moeryomenko%2Fsquad/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moeryomenko%2Fsquad/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/moeryomenko","download_url":"https://codeload.github.com/moeryomenko/squad/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moeryomenko%2Fsquad/sbom","scorecard":{"id":656309,"data":{"date":"2025-08-11","repo":{"name":"github.com/moeryomenko/squad","commit":"d9fc6745bdff6def9c99c4cf2904f42eff9fbd45"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.6,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Maintained","score":5,"reason":"6 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 5","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE-APACHE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE-APACHE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":9,"reason":"1 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GO-2025-3787 / GHSA-fv92-fjc5-jj9h"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-21T14:44:15.023Z","repository_id":39092373,"created_at":"2025-08-21T14:44:15.023Z","updated_at":"2025-08-21T14:44:15.023Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28338095,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-12T06:09:07.588Z","status":"ssl_error","status_checked_at":"2026-01-12T06:05:18.301Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["errgroup","golang","graceful-shutdown"],"created_at":"2026-01-12T10:18:03.558Z","updated_at":"2026-01-12T10:18:04.396Z","avatar_url":"https://github.com/moeryomenko.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# 🚀 Squad\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/moeryomenko/squad.svg)](https://pkg.go.dev/github.com/moeryomenko/squad)\n[![Go Report Card](https://goreportcard.com/badge/github.com/moeryomenko/squad)](https://goreportcard.com/report/github.com/moeryomenko/squad)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE-MIT)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE-APACHE)\n\n**A shared shutdown primitive for graceful application lifecycle management in Go**\n\nSquad helps you orchestrate goroutines, manage graceful shutdowns, and handle application lifecycle events with minimal boilerplate.\n\n[Features](#-features) • [Installation](#-installation) • [Quick Start](#-quick-start) • [Documentation](#-documentation) • [Examples](#-examples)\n\n\u003c/div\u003e\n\n---\n\n## 📋 Table of Contents\n\n- [About](#-about)\n- [Features](#-features)\n- [Installation](#-installation)\n- [Quick Start](#-quick-start)\n- [Usage](#-usage)\n  - [Basic Service](#basic-service)\n  - [HTTP Server](#http-server)\n  - [Consumer Workers](#consumer-workers)\n  - [Graceful Shutdown](#graceful-shutdown)\n  - [Bootstrap \u0026 Cleanup](#bootstrap--cleanup)\n- [API Reference](#-api-reference)\n- [Examples](#-examples)\n- [Project Structure](#-project-structure)\n- [Development](#-development)\n- [Testing](#-testing)\n- [FAQ](#-faq)\n- [Contributing](#-contributing)\n- [License](#-license)\n- [Author](#-author)\n\n---\n\n## 🎯 About\n\n**Squad** is a lightweight Go package that provides a shared shutdown primitive for managing application lifecycle. It allows you to:\n\n- **Coordinate multiple goroutines** that must start and stop together\n- **Handle graceful shutdowns** with configurable timeouts and grace periods\n- **Manage HTTP servers** with proper shutdown sequences\n- **Run consumer workers** (e.g., message queues, event streams) with graceful stop\n- **Execute bootstrap and cleanup functions** for subsystems\n- **Automatically handle OS signals** (SIGINT, SIGTERM, SIGHUP, SIGQUIT)\n\nSquad is particularly useful for:\n- Microservices and API servers\n- Background workers and job processors\n- Event-driven applications with multiple consumers\n- Any Go application requiring coordinated startup/shutdown\n\nThe package is designed to be Kubernetes-aware, with default grace periods aligned with Pod termination lifecycle.\n\n---\n\n## ✨ Features\n\n- ⚡ **Zero dependencies** - Uses only standard library and `github.com/moeryomenko/synx`\n- 🔄 **Coordinated lifecycle** - If one goroutine exits, others are signaled to stop\n- 🛡️ **Graceful shutdowns** - Configurable grace periods and shutdown timeouts\n- 🌐 **HTTP server support** - Built-in wrapper for `http.Server` with proper shutdown\n- 📨 **Consumer pattern** - Graceful stop for message/event consumers without interrupting active handlers\n- 🎯 **Signal handling** - Automatic SIGINT/SIGTERM/SIGHUP/SIGQUIT handling\n- 🔌 **Bootstrap/cleanup hooks** - Run initialization and cleanup functions\n- ⏱️ **Kubernetes-friendly** - Default 30s grace period matches k8s pod termination\n- 🧪 **Well tested** - Comprehensive test coverage\n- 📦 **Simple API** - Minimal boilerplate, intuitive usage\n\n---\n\n## 📦 Installation\n\n```bash\ngo get github.com/moeryomenko/squad\n```\n\n**Requirements:**\n- Go 1.25 or higher\n\n---\n\n## 🚀 Quick Start\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"log\"\n\n    \"github.com/moeryomenko/squad\"\n)\n\nfunc main() {\n    // Create a new squad with signal handler\n    s, err := squad.New(squad.WithSignalHandler())\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    // Run your application logic\n    s.Run(func(ctx context.Context) error {\n        // Your code here\n        \u003c-ctx.Done()\n        return nil\n    })\n\n    // Wait for shutdown\n    if err := s.Wait(); err != nil {\n        log.Printf(\"shutdown error: %v\", err)\n    }\n}\n```\n\n---\n\n## 📖 Usage\n\n### Basic Service\n\nCreate a simple service with signal handling:\n\n```go\ns, err := squad.New(squad.WithSignalHandler())\nif err != nil {\n    log.Fatal(err)\n}\n\ns.Run(func(ctx context.Context) error {\n    // Your background work\n    ticker := time.NewTicker(time.Second)\n    defer ticker.Stop()\n\n    for {\n        select {\n        case \u003c-ctx.Done():\n            return nil\n        case \u003c-ticker.C:\n            log.Println(\"tick\")\n        }\n    }\n})\n\ns.Wait()\n```\n\n### HTTP Server\n\nLaunch an HTTP server with graceful shutdown:\n\n```go\ns, err := squad.New(squad.WithSignalHandler())\nif err != nil {\n    log.Fatal(err)\n}\n\nhttp.HandleFunc(\"/health\", func(w http.ResponseWriter, r *http.Request) {\n    w.WriteHeader(http.StatusOK)\n})\n\ns.RunServer(\u0026http.Server{Addr: \":8080\"})\ns.Wait()\n```\n\n### Consumer Workers\n\nRun consumer workers that stop gracefully without interrupting active handlers:\n\n```go\ns, err := squad.New(squad.WithSignalHandler())\nif err != nil {\n    log.Fatal(err)\n}\n\ns.RunConsumer(func(consumeCtx, handleCtx context.Context) error {\n    // consumeCtx: cancelled when shutdown starts\n    // handleCtx: never cancelled, allows active handlers to complete\n\n    for {\n        select {\n        case \u003c-consumeCtx.Done():\n            return nil\n        default:\n            msg := receiveMessage() // Your message source\n            go processMessage(handleCtx, msg)\n        }\n    }\n})\n\ns.Wait()\n```\n\n### Graceful Shutdown\n\nRun tasks with cleanup functions:\n\n```go\ns, err := squad.New(squad.WithSignalHandler(\n    squad.WithGracefulPeriod(30 * time.Second),\n    squad.WithShutdownTimeout(5 * time.Second),\n))\nif err != nil {\n    log.Fatal(err)\n}\n\ns.RunGracefully(\n    // Background function\n    func(ctx context.Context) error {\n        // Your work\n        \u003c-ctx.Done()\n        return nil\n    },\n    // Cleanup function (runs on shutdown)\n    func(ctx context.Context) error {\n        log.Println(\"cleaning up...\")\n        return closeResources(ctx)\n    },\n)\n\ns.Wait()\n```\n\n### Bootstrap \u0026 Cleanup\n\nInitialize and cleanup subsystems:\n\n```go\ns, err := squad.New(\n    squad.WithSignalHandler(),\n    squad.WithBootstrap(\n        func(ctx context.Context) error {\n            return initDatabase(ctx)\n        },\n        func(ctx context.Context) error {\n            return connectToCache(ctx)\n        },\n    ),\n    squad.WithCloses(\n        func(ctx context.Context) error {\n            return closeDatabase(ctx)\n        },\n    ),\n    squad.WithSubsystem(\n        func(ctx context.Context) error { return openMessageQueue(ctx) },\n        func(ctx context.Context) error { return closeMessageQueue(ctx) },\n    ),\n)\nif err != nil {\n    log.Fatal(\"initialization failed:\", err)\n}\n\n// Your application code...\n\ns.Wait()\n```\n\n---\n\n## 🔍 API Reference\n\n### Core Types\n\n- **`Squad`** - Main struct for coordinating goroutines\n- **`ConsumerLoop`** - Function signature for consumer workers\n\n### Constructor\n\n- **`New(opts ...Option) (*Squad, error)`** - Create a new Squad instance\n\n### Options\n\n- **`WithSignalHandler(...ShutdownOpt)`** - Add OS signal handling\n- **`WithGracefulPeriod(duration)`** - Set graceful shutdown period (default: 30s)\n- **`WithShutdownTimeout(duration)`** - Set shutdown timeout (default: 2s)\n- **`WithShutdownInGracePeriod(duration)`** - Set both graceful and shutdown timeout\n- **`WithBootstrap(...func)`** - Add initialization functions\n- **`WithCloses(...func)`** - Add cleanup functions\n- **`WithSubsystem(init, close)`** - Add init+cleanup pair for a subsystem\n\n### Methods\n\n- **`Run(fn)`** - Run a function in the squad\n- **`RunGracefully(backgroundFn, onDown)`** - Run with cleanup function\n- **`RunServer(*http.Server)`** - Run HTTP server with graceful shutdown\n- **`RunConsumer(ConsumerLoop)`** - Run consumer worker\n- **`Wait() error`** - Block until all squad members exit\n\n---\n\n## 💡 Examples\n\nSee the [`example/`](./example) directory for complete examples:\n\n- **[simple.go](./example/simple.go)** - HTTP server with signal handling and graceful shutdown\n\nRun the example:\n\n```bash\ngo run example/simple.go\n```\n\n---\n\n## 📁 Project Structure\n\n```\nsquad/\n├── squad.go           # Core Squad implementation\n├── consumers.go       # Consumer worker helpers\n├── options.go         # Configuration options\n├── squad_test.go      # Unit tests\n├── example/           # Example applications\n│   └── simple.go\n├── tools/             # Development tools\n├── go.mod             # Go module definition\n├── Makefile           # Build automation\n├── LICENSE-APACHE     # Apache 2.0 license\n├── LICENSE-MIT        # MIT license\n└── README.md          # This file\n```\n\n---\n\n## 🛠 Development\n\n### Prerequisites\n\n- Go 1.25+\n- golangci-lint (for linting)\n- go-mod-upgrade (for dependency management)\n\n### Commands\n\n```bash\n# Run tests\nmake test\n\n# Run tests with race detector\nRACE_DETECTOR=1 make test\n\n# View coverage report\nmake cover\n\n# Run linter\nmake lint\n\n# Update dependencies\nmake mod\n\n# View all commands\nmake help\n```\n\n---\n\n## 🧪 Testing\n\nSquad includes comprehensive unit tests covering:\n\n- Basic lifecycle management\n- Bootstrap initialization\n- Background task failures\n- Shutdown failures and timeouts\n- Error propagation\n- Concurrent operations\n\nRun tests:\n\n```bash\ngo test ./...\n\n# With race detector\ngo test -race ./...\n\n# With coverage\ngo test -coverprofile=coverage.out ./...\ngo tool cover -html=coverage.out\n```\n\n---\n\n## ❓ FAQ\n\n### Q: What happens if a goroutine panics?\n\nSquad uses `synx.CtxGroup` which recovers from panics and converts them to errors. The error will be returned from `Wait()`.\n\n### Q: What's the difference between graceful period and shutdown timeout?\n\n- **Graceful period**: Total time allowed for the entire shutdown process\n- **Shutdown timeout**: Time reserved for executing cleanup functions\n\nThe signal handler waits `gracefulPeriod - shutdownTimeout` before canceling the squad context.\n\n### Q: Can I use Squad without signal handling?\n\nYes! Simply don't use `WithSignalHandler()`. You'll need to manage context cancellation yourself.\n\n### Q: How does RunConsumer differ from Run?\n\n`RunConsumer` provides two contexts:\n- `consumeContext`: Cancelled on shutdown (stop accepting new work)\n- `handleContext`: Never cancelled (allow in-flight work to complete)\n\nThis enables graceful consumer shutdown without interrupting active message handlers.\n\n### Q: Is Squad safe for concurrent use?\n\nYes, Squad is designed to be used concurrently. Internal state is protected by mutexes.\n\n### Q: What's the default grace period?\n\n30 seconds, matching Kubernetes pod termination grace period. This ensures compatibility with k8s deployments.\n\n---\n\n## 🤝 Contributing\n\nContributions are welcome! Please feel free to submit issues, fork the repository, and send pull requests.\n\n### Guidelines\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Make your changes\n4. Add tests for new functionality\n5. Run tests and linter (`make test lint`)\n6. Commit your changes\n7. Push to the branch (`git push origin feature/amazing-feature`)\n8. Open a Pull Request\n\n---\n\n## 📄 License\n\nThis project is dual-licensed under your choice of:\n\n- **MIT License** - See [LICENSE-MIT](./LICENSE-MIT)\n- **Apache License 2.0** - See [LICENSE-APACHE](./LICENSE-APACHE)\n\nYou may use this project under the terms of either license.\n\n---\n\n## 👤 Author\n\n**Maxim Eryomenko**\n\n- GitHub: [@moeryomenko](https://github.com/moeryomenko)\n- Email: moeryomenko@gmail.com\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\n**If you find Squad useful, please consider giving it a ⭐️!**\n\nMade with ❤️ by [Maxim Eryomenko](https://github.com/moeryomenko)\n\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmoeryomenko%2Fsquad","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmoeryomenko%2Fsquad","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmoeryomenko%2Fsquad/lists"}