├── container ├── test │ ├── without_endure_ok.yaml │ ├── endure_ok.yaml │ ├── endure_ok_info.yaml │ ├── endure_ok_warn.yaml │ ├── endure_ok_debug.yaml │ ├── endure_ok_error.yaml │ └── endure_ok_foobar.yaml ├── plugins_test.go ├── container_test.go ├── config.go ├── config_test.go └── plugins.go ├── internal ├── rpc │ ├── test │ │ ├── config_rpc_empty.yaml │ │ ├── config_rpc_wrong.yaml │ │ ├── config_rpc_ok_env.yaml │ │ ├── config_rpc_ok.yaml │ │ ├── config_rpc_conn_err.yaml │ │ └── include1 │ │ │ ├── .rr-include.yaml │ │ │ └── .rr.yaml │ ├── includes.go │ ├── client_test.go │ └── client.go ├── cli │ ├── jobs │ │ ├── command_test.go │ │ ├── render.go │ │ ├── subcommands.go │ │ └── command.go │ ├── reset │ │ ├── command_test.go │ │ └── command.go │ ├── stop │ │ ├── command_test.go │ │ └── command.go │ ├── serve │ │ ├── command_test.go │ │ ├── command_windows.go │ │ └── command.go │ ├── workers │ │ ├── command_test.go │ │ ├── command.go │ │ └── render.go │ ├── root_test.go │ └── root.go ├── meta │ ├── meta.go │ └── meta_test.go ├── debug │ ├── server.go │ └── server_test.go └── sdnotify │ └── sdnotify.go ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── chore.yml │ ├── feature-request.yml │ └── bug-report.yml ├── workflows │ ├── semgrep.yml │ ├── dependency-review.yml │ ├── codeql-analysis.yml │ ├── tests.yml │ ├── release_dep.yml │ ├── release_dep_aarch64.yml │ ├── release_grpc.yml │ └── release.yml ├── pull_request_template.md └── dependabot.yml ├── CHANGELOG.md ├── githooks-installer.sh ├── .dockerignore ├── Makefile ├── SECURITY.md ├── codecov.yml ├── .editorconfig ├── .githooks └── pre-commit ├── schemas ├── package.json ├── readme.md ├── test.js ├── package-lock.json └── config │ └── 1.0.schema.json ├── .gitignore ├── cmd └── rr │ ├── main.go │ └── command_test.go ├── .vscode └── launch.json ├── composer.json ├── LICENSE ├── .golangci.yml ├── Dockerfile ├── lib ├── roadrunner_test.go └── roadrunner.go ├── benchmarks └── simple.js ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── download-latest.sh ├── CLAUDE.md ├── README.md └── go.mod /container/test/without_endure_ok.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/rpc/test/config_rpc_empty.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Primary owners 2 | 3 | @wolfy-j @rustatian 4 | -------------------------------------------------------------------------------- /internal/rpc/test/config_rpc_wrong.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | rpc: 4 | $foo bar 5 | -------------------------------------------------------------------------------- /internal/rpc/test/config_rpc_ok_env.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | rpc: 4 | listen: ${RPC} 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | releases: [docs](https://docs.roadrunner.dev/docs/releases) 4 | -------------------------------------------------------------------------------- /internal/rpc/test/config_rpc_ok.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | rpc: 4 | listen: tcp://127.0.0.1:55554 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: roadrunner-server 4 | -------------------------------------------------------------------------------- /internal/rpc/test/config_rpc_conn_err.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | rpc: 4 | listen: tcp://127.0.0.1:55554 5 | -------------------------------------------------------------------------------- /internal/rpc/test/include1/.rr-include.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | rpc: 4 | listen: tcp://127.0.0.1:6010 5 | 6 | -------------------------------------------------------------------------------- /githooks-installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cp ./.githooks/pre-commit .git/hooks/pre-commit 6 | 7 | echo "DONE" -------------------------------------------------------------------------------- /container/test/endure_ok.yaml: -------------------------------------------------------------------------------- 1 | endure: 2 | grace_period: 10s 3 | print_graph: true 4 | retry_on_fail: true 5 | log_level: warn 6 | -------------------------------------------------------------------------------- /container/test/endure_ok_info.yaml: -------------------------------------------------------------------------------- 1 | endure: 2 | grace_period: 10s 3 | print_graph: true 4 | retry_on_fail: true 5 | log_level: info 6 | -------------------------------------------------------------------------------- /container/test/endure_ok_warn.yaml: -------------------------------------------------------------------------------- 1 | endure: 2 | grace_period: 10s 3 | print_graph: true 4 | retry_on_fail: true 5 | log_level: warn 6 | -------------------------------------------------------------------------------- /container/test/endure_ok_debug.yaml: -------------------------------------------------------------------------------- 1 | endure: 2 | grace_period: 10s 3 | print_graph: true 4 | retry_on_fail: true 5 | log_level: debug 6 | -------------------------------------------------------------------------------- /container/test/endure_ok_error.yaml: -------------------------------------------------------------------------------- 1 | endure: 2 | grace_period: 10s 3 | print_graph: true 4 | retry_on_fail: true 5 | log_level: error 6 | -------------------------------------------------------------------------------- /container/test/endure_ok_foobar.yaml: -------------------------------------------------------------------------------- 1 | endure: 2 | grace_period: 10s 3 | print_graph: true 4 | retry_on_fail: true 5 | log_level: foobar 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git 3 | .gitignore 4 | .editorconfig 5 | .github 6 | /src 7 | /tests 8 | /bin 9 | composer.json 10 | vendor_php 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test -v -race ./... 3 | 4 | build: 5 | CGO_ENABLED=0 go build -trimpath -ldflags "-s" -o rr cmd/rr/main.go 6 | 7 | debug: 8 | dlv debug cmd/rr/main.go -- serve -c .rr-sample-bench-http.yaml 9 | -------------------------------------------------------------------------------- /internal/rpc/test/include1/.rr.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | server: 4 | command: "php app-with-domain-specific-routes.php" 5 | 6 | logs: 7 | level: debug 8 | mode: development 9 | 10 | include: 11 | - test/include1/.rr-include.yaml 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | |----------|-----------| 7 | | 2.x | No | 8 | | 2023.x.x | No | 9 | | 2024.x.x | Yes | 10 | | 2025.x.x | Yes | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: ❓ Start a discussion or ask a question. 5 | url: https://github.com/roadrunner-server/roadrunner/discussions 6 | about: Please ask and answer questions here. 7 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 0% 7 | informational: true 8 | patch: 9 | default: 10 | target: auto 11 | threshold: 0% 12 | informational: true 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = tab 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.{yml,yaml,sh,conf}] 12 | indent_size = 2 13 | 14 | [{Makefile,go.mod,*.go}] 15 | indent_style = tab 16 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | # https://github.com/koalaman/shellcheck/wiki/SC2039#redirect-both-stdout-and-stderr 6 | if ! command -v golangci-lint 2>&1 /dev/null; then 7 | echo "golangci-lint is not installed" 8 | exit 1 9 | fi 10 | 11 | exec golangci-lint --build-tags=race run "$@" 12 | -------------------------------------------------------------------------------- /schemas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roadrunner-schema-tests", 3 | "version": "1.0.0", 4 | "main": "test.js", 5 | "type": "module", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@apidevtools/json-schema-ref-parser": "^11.7.2", 13 | "ajv": "^8.17.1", 14 | "js-yaml": "^4.1.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/cli/jobs/command_test.go: -------------------------------------------------------------------------------- 1 | package jobs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli/jobs" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCommandProperties(t *testing.T) { 11 | path := "" 12 | f := false 13 | cmd := jobs.NewCommand(&path, nil, &f) 14 | 15 | assert.Equal(t, "jobs", cmd.Use) 16 | assert.NotNil(t, cmd.RunE) 17 | } 18 | -------------------------------------------------------------------------------- /internal/cli/reset/command_test.go: -------------------------------------------------------------------------------- 1 | package reset_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli/reset" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommandProperties(t *testing.T) { 12 | path := "" 13 | f := false 14 | cmd := reset.NewCommand(&path, nil, &f) 15 | 16 | assert.Equal(t, "reset", cmd.Use) 17 | assert.NotNil(t, cmd.RunE) 18 | } 19 | -------------------------------------------------------------------------------- /container/plugins_test.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestPlugins(t *testing.T) { 9 | for _, p := range Plugins() { 10 | if p == nil { 11 | t.Error("plugin cannot be nil") 12 | } 13 | 14 | if pk := reflect.TypeOf(p).Kind(); pk != reflect.Ptr && pk != reflect.Struct { 15 | t.Errorf("plugin %v must be a structure or pointer to the structure", p) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /schemas/readme.md: -------------------------------------------------------------------------------- 1 | # Schemas 2 | 3 | This directory contains public schemas for the most important parts of application. 4 | 5 | **Do not rename or remove this directory or any file or directory inside.** 6 | 7 | - You can validate existing config file using the following command from the project root. 8 | 9 | ```bash 10 | docker run --rm -v "$(pwd):/src" -w "/src" node:22-alpine sh -c \ 11 | "cd schemas && npm install && node test.js" 12 | ``` 13 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: semgrep 2 | on: 3 | pull_request: {} 4 | push: 5 | branches: 6 | - master 7 | - stable 8 | paths: 9 | - .github/workflows/semgrep.yml 10 | jobs: 11 | semgrep: 12 | name: semgrep/ci 13 | runs-on: ubuntu-latest 14 | env: 15 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 16 | container: 17 | image: returntocorp/semgrep 18 | steps: 19 | - uses: actions/checkout@v6 20 | - run: semgrep ci 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | .idea 19 | coverage 20 | /rr 21 | rr.exe 22 | .rr-sample-* 23 | .pid 24 | .DS_Store 25 | **/.DS_Store 26 | **/node_modules 27 | -------------------------------------------------------------------------------- /container/container_test.go: -------------------------------------------------------------------------------- 1 | package container_test 2 | 3 | import ( 4 | "log/slog" 5 | "testing" 6 | "time" 7 | 8 | "github.com/roadrunner-server/endure/v2" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewContainer(t *testing.T) { // there is no legal way to test container options 13 | c := endure.New(slog.LevelDebug, endure.Visualize(), endure.GracefulShutdownTimeout(time.Second)) 14 | c2 := endure.New(slog.LevelDebug, endure.Visualize(), endure.GracefulShutdownTimeout(time.Second)) 15 | 16 | assert.NotNil(t, c) 17 | 18 | assert.NotNil(t, c2) 19 | } 20 | -------------------------------------------------------------------------------- /internal/meta/meta.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import "strings" 4 | 5 | // next variables will be set during compilation (do NOT rename them). 6 | var ( 7 | version = "local" 8 | buildTime = "development" //nolint:gochecknoglobals 9 | ) 10 | 11 | // Version returns version value (without `v` prefix). 12 | func Version() string { 13 | v := strings.TrimSpace(version) 14 | 15 | if len(v) > 1 && ((v[0] == 'v' || v[0] == 'V') && (v[1] >= '0' && v[1] <= '9')) { 16 | return v[1:] 17 | } 18 | 19 | return v 20 | } 21 | 22 | // BuildTime returns application building time. 23 | func BuildTime() string { return buildTime } 24 | -------------------------------------------------------------------------------- /internal/cli/stop/command_test.go: -------------------------------------------------------------------------------- 1 | package stop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli/stop" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommandProperties(t *testing.T) { 12 | cmd := stop.NewCommand(toPtr(false), toPtr(false)) 13 | 14 | assert.Equal(t, "stop", cmd.Use) 15 | assert.NotNil(t, cmd.RunE) 16 | } 17 | 18 | func TestCommandTrue(t *testing.T) { 19 | cmd := stop.NewCommand(toPtr(true), toPtr(true)) 20 | 21 | assert.Equal(t, "stop", cmd.Use) 22 | assert.NotNil(t, cmd.RunE) 23 | } 24 | 25 | func toPtr[T any](val T) *T { 26 | return &val 27 | } 28 | -------------------------------------------------------------------------------- /cmd/rr/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/fatih/color" 8 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli" 9 | ) 10 | 11 | // exitFn is a function for an application exiting. 12 | var exitFn = os.Exit //nolint:gochecknoglobals 13 | 14 | // main CLI application entrypoint. 15 | func main() { exitFn(run()) } 16 | 17 | // run this CLI application. 18 | func run() int { 19 | cmd := cli.NewCommand(filepath.Base(os.Args[0])) 20 | 21 | if err := cmd.Execute(); err != nil { 22 | _, _ = color.New(color.FgHiRed, color.Bold).Fprintln(os.Stderr, err.Error()) 23 | 24 | return 1 25 | } 26 | 27 | return 0 28 | } 29 | -------------------------------------------------------------------------------- /internal/cli/serve/command_test.go: -------------------------------------------------------------------------------- 1 | package serve_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli/serve" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommandProperties(t *testing.T) { 12 | path := "" 13 | cmd := serve.NewCommand(nil, &path, nil, nil) 14 | 15 | assert.Equal(t, "serve", cmd.Use) 16 | assert.NotNil(t, cmd.RunE) 17 | } 18 | 19 | func TestCommandNil(t *testing.T) { 20 | cmd := serve.NewCommand(nil, nil, nil, nil) 21 | 22 | assert.Equal(t, "serve", cmd.Use) 23 | assert.NotNil(t, cmd.RunE) 24 | } 25 | 26 | func TestExecution(t *testing.T) { 27 | t.Skip("Command execution is not implemented yet") 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.yml: -------------------------------------------------------------------------------- 1 | name: Chore 2 | description: 🧹 Enhancement or chore of the existing code 3 | title: "[🧹 CHORE]: " 4 | labels: ["C-enhancement"] 5 | assignees: 6 | - rustatian 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this report! 12 | 13 | - type: checkboxes 14 | id: search-done 15 | attributes: 16 | label: No duplicates 🥲. 17 | options: 18 | - label: I have searched for a similar issue. 19 | required: true 20 | - type: textarea 21 | id: what-happened 22 | attributes: 23 | label: What should be improved or cleaned up? 24 | description: Also tell us, what did you expect to happen? 25 | placeholder: Tell us what you see! 26 | validations: 27 | required: true 28 | -------------------------------------------------------------------------------- /internal/cli/jobs/render.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/olekukonko/tablewriter" 7 | "github.com/olekukonko/tablewriter/tw" 8 | ) 9 | 10 | // JobsCommandsRender uses console renderer to show jobs 11 | func renderPipelines(writer io.Writer, pipelines []string) *tablewriter.Table { 12 | cfg := tablewriter.Config{ 13 | Header: tw.CellConfig{ 14 | Formatting: tw.CellFormatting{ 15 | AutoFormat: tw.On, 16 | AutoWrap: int(tw.Off), 17 | }, 18 | }, 19 | MaxWidth: 150, 20 | Row: tw.CellConfig{ 21 | Alignment: tw.CellAlignment{ 22 | Global: tw.AlignLeft, 23 | }, 24 | }, 25 | } 26 | tw := tablewriter.NewTable(writer, tablewriter.WithConfig(cfg)) 27 | tw.Header([]string{"Pipeline(s)"}) 28 | 29 | for i := range pipelines { 30 | _ = tw.Append([]string{pipelines[i]}) 31 | } 32 | 33 | return tw 34 | } 35 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Reason for This PR 2 | 3 | `[Author TODO: add issue # or explain reasoning.]` 4 | 5 | ## Description of Changes 6 | 7 | `[Author TODO: add description of changes.]` 8 | 9 | ## License Acceptance 10 | 11 | By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license. 12 | 13 | ## PR Checklist 14 | 15 | `[Author TODO: Meet these criteria.]` 16 | `[Reviewer TODO: Verify that these criteria are met. Request changes if not]` 17 | 18 | - [ ] All commits in this PR are signed (`git commit -s`). 19 | - [ ] The reason for this PR is clearly provided (issue no. or explanation). 20 | - [ ] The description of changes is clear and encompassing. 21 | - [ ] Any required documentation changes (code and docs) are included in this PR. 22 | - [ ] Any user-facing changes are mentioned in `CHANGELOG.md`. 23 | - [ ] All added/changed functionality is tested. 24 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v6 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v4 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | version: 2 6 | 7 | updates: 8 | - package-ecosystem: gomod # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: daily 12 | reviewers: 13 | - "rustatian" 14 | assignees: 15 | - "rustatian" 16 | 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: daily 21 | reviewers: 22 | - "rustatian" 23 | assignees: 24 | - "rustatian" 25 | 26 | - package-ecosystem: "docker" 27 | directory: "/" 28 | schedule: 29 | interval: daily 30 | reviewers: 31 | - "rustatian" 32 | assignees: 33 | - "rustatian" 34 | -------------------------------------------------------------------------------- /internal/debug/server.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/pprof" 7 | "time" 8 | ) 9 | 10 | // Server is a HTTP server for debugging. 11 | type Server struct { 12 | srv *http.Server 13 | } 14 | 15 | // NewServer creates new HTTP server for debugging. 16 | func NewServer() Server { 17 | mux := http.NewServeMux() 18 | 19 | mux.HandleFunc("/debug/pprof/", pprof.Index) 20 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 21 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 22 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 23 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 24 | 25 | return Server{srv: &http.Server{ 26 | ReadHeaderTimeout: time.Minute * 10, 27 | Handler: mux, 28 | }} 29 | } 30 | 31 | // Start debug server. 32 | func (s *Server) Start(addr string) error { 33 | s.srv.Addr = addr 34 | 35 | return s.srv.ListenAndServe() 36 | } 37 | 38 | // Stop debug server. 39 | func (s *Server) Stop(ctx context.Context) error { 40 | return s.srv.Shutdown(ctx) 41 | } 42 | -------------------------------------------------------------------------------- /internal/meta/meta_test.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestVersion(t *testing.T) { 10 | for give, want := range map[string]string{ 11 | // without changes 12 | "vvv": "vvv", 13 | "victory": "victory", 14 | "voodoo": "voodoo", 15 | "foo": "foo", 16 | "0.0.0": "0.0.0", 17 | "v": "v", 18 | "V": "V", 19 | 20 | // "v" prefix removal 21 | "v0.0.0": "0.0.0", 22 | "V0.0.0": "0.0.0", 23 | "v1": "1", 24 | "V1": "1", 25 | 26 | // with spaces 27 | " 0.0.0": "0.0.0", 28 | "v0.0.0 ": "0.0.0", 29 | " V0.0.0": "0.0.0", 30 | "v1 ": "1", 31 | " V1": "1", 32 | "v ": "v", 33 | } { 34 | version = give 35 | 36 | assert.Equal(t, want, Version()) 37 | } 38 | } 39 | 40 | func TestBuildTime(t *testing.T) { 41 | for give, want := range map[string]string{ 42 | "development": "development", 43 | "2021-03-26T13:50:31+0500": "2021-03-26T13:50:31+0500", 44 | } { 45 | buildTime = give 46 | 47 | assert.Equal(t, want, BuildTime()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Start RR with AMQP", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/cmd/rr/main.go", 13 | "args": [ 14 | "serve", 15 | "-c", 16 | "../../.rr-sample-bench-jobs.yaml" 17 | ], 18 | }, 19 | { 20 | "name": "Start RR with HTTP", 21 | "type": "go", 22 | "request": "launch", 23 | "mode": "auto", 24 | "program": "${workspaceFolder}/cmd/rr/main.go", 25 | "args": [ 26 | "serve", 27 | "-c", 28 | "../../.rr-sample-bench-http.yaml" 29 | ], 30 | }, 31 | { 32 | "name": "RR workers", 33 | "type": "go", 34 | "request": "launch", 35 | "mode": "auto", 36 | "program": "${workspaceFolder}/cmd/rr/main.go", 37 | "args": [ 38 | "workers", 39 | "-c", 40 | "../../.rr-sample-bench-http.yaml" 41 | ], 42 | }, 43 | ] 44 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spiral/roadrunner", 3 | "type": "metapackage", 4 | "description": "RoadRunner: High-performance PHP application server and process manager written in Go and powered with plugins", 5 | "license": "MIT", 6 | "homepage": "https://roadrunner.dev/", 7 | "support": { 8 | "docs": "https://docs.roadrunner.dev/", 9 | "issues": "https://github.com/roadrunner-server/roadrunner/issues", 10 | "chat": "https://discord.gg/V6EK4he" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Anton Titov / Wolfy-J", 15 | "email": "wolfy.jd@gmail.com" 16 | }, 17 | { 18 | "name": "Valery Piashchynski", 19 | "homepage": "https://github.com/rustatian" 20 | }, 21 | { 22 | "name": "RoadRunner Community", 23 | "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" 24 | } 25 | ], 26 | "funding": [ 27 | { 28 | "type": "github", 29 | "url": "https://github.com/sponsors/roadrunner-server" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Spiral Scout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: 💡 Suggest an idea for this project 3 | title: "[💡 FEATURE REQUEST]: " 4 | labels: ["C-feature-request"] 5 | assignees: 6 | - rustatian 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to share your idea! 12 | 13 | - type: dropdown 14 | id: plugin 15 | attributes: 16 | label: Plugin 17 | description: What plugin is affected? 18 | options: 19 | - GRPC 20 | - HTTP 21 | - HTTP Middleware (any) 22 | - Jobs 23 | - Jobs driver 24 | - TCP 25 | - File server 26 | - Config 27 | - KV 28 | - KV driver 29 | - Logger 30 | - Metrics 31 | - Temporal 32 | - Service 33 | - Server 34 | - Status 35 | - Centrifuge 36 | 37 | - type: textarea 38 | id: idea 39 | attributes: 40 | label: I have an idea! 41 | description: Clear and concise description of your idea. 42 | placeholder: Tell us what you see! 43 | value: "I have an idea, listen to me!!" 44 | validations: 45 | required: true 46 | -------------------------------------------------------------------------------- /internal/cli/workers/command_test.go: -------------------------------------------------------------------------------- 1 | package workers_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli/workers" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCommandProperties(t *testing.T) { 12 | cmd := workers.NewCommand(nil, nil) 13 | 14 | assert.Equal(t, "workers", cmd.Use) 15 | assert.NotNil(t, cmd.RunE) 16 | } 17 | 18 | func TestCommandFlags(t *testing.T) { 19 | cmd := workers.NewCommand(nil, nil) 20 | 21 | cases := []struct { 22 | giveName string 23 | wantShorthand string 24 | wantDefault string 25 | }{ 26 | {giveName: "interactive", wantShorthand: "i", wantDefault: "false"}, 27 | } 28 | 29 | for _, tt := range cases { 30 | t.Run(tt.giveName, func(t *testing.T) { 31 | flag := cmd.Flag(tt.giveName) 32 | 33 | if flag == nil { 34 | assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName) 35 | 36 | return 37 | } 38 | 39 | assert.Equal(t, tt.wantShorthand, flag.Shorthand) 40 | assert.Equal(t, tt.wantDefault, flag.DefValue) 41 | }) 42 | } 43 | } 44 | 45 | func TestExecution(t *testing.T) { 46 | t.Skip("Command execution is not implemented yet") 47 | } 48 | -------------------------------------------------------------------------------- /schemas/test.js: -------------------------------------------------------------------------------- 1 | import $RefParser from "@apidevtools/json-schema-ref-parser"; 2 | import Ajv2019 from "ajv/dist/2019.js" 3 | import fs from 'fs'; 4 | import yaml from 'js-yaml'; 5 | 6 | function stripIds(schema, first) { 7 | if (schema !== null && typeof schema === 'object') { 8 | if (!first) { 9 | // Every referenced schema we pull in should have its $id and $schema stripped, or ajv complains 10 | // Skip the root object, as that should retain the $schema and $id 11 | delete schema.$id; 12 | delete schema.$schema; 13 | } 14 | for (const key in schema) { 15 | if (Object.hasOwn(schema, key)) { 16 | stripIds(schema[key], false); 17 | } 18 | } 19 | } 20 | } 21 | 22 | // Load the main schema and all its referenced schemas 23 | const dereferenced = await $RefParser.dereference('./config/3.0.schema.json'); 24 | 25 | // Remove $id and $schema from anything but the root 26 | stripIds(dereferenced, true); 27 | 28 | const ajv = new Ajv2019({strict: true, allErrors: true}) 29 | const validator = ajv.compile(dereferenced) 30 | 31 | const data = fs.readFileSync('../.rr.yaml', 'utf-8'); 32 | const schema = yaml.load(data); 33 | 34 | // Validate the file 35 | if (!validator(schema)) { 36 | throw new Error(JSON.stringify(validator.errors, null, 2)) 37 | } else { 38 | console.log('No errors found in schemas.') 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need to commit it to your repository. 2 | # 3 | # You may wish to alter this file to override the set of languages analyzed, or to provide custom queries or build logic. 4 | name: "CodeQL" 5 | 6 | on: 7 | push: 8 | branches: [ master, stable ] 9 | pull_request: 10 | branches: [ master, stable ] 11 | schedule: 12 | - cron: '0 15 * * 6' 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | language: [ 'go' ] # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v6 25 | with: 26 | # We must fetch at least the immediate parents so that if this is a pull request then we can checkout the head 27 | fetch-depth: 2 28 | 29 | # Initializes the Golang environment for the CodeQL tools. 30 | # https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087 31 | - name: Install Go 32 | uses: actions/setup-go@v6 33 | with: 34 | go-version-file: go.mod 35 | 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v4 38 | with: 39 | languages: ${{ matrix.language }} 40 | 41 | - name: Perform CodeQL Analysis 42 | uses: github/codeql-action/analyze@v4 43 | -------------------------------------------------------------------------------- /internal/cli/jobs/subcommands.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "net/rpc" 5 | "os" 6 | 7 | jobsv1 "github.com/roadrunner-server/api/v4/build/jobs/v1" 8 | ) 9 | 10 | func pause(client *rpc.Client, pause []string, silent *bool) error { 11 | pipes := &jobsv1.Pipelines{Pipelines: pause} 12 | er := &jobsv1.Empty{} 13 | 14 | err := client.Call(pauseRPC, pipes, er) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | if !*silent { 20 | _ = renderPipelines(os.Stdout, pause).Render() 21 | } 22 | 23 | return nil 24 | } 25 | 26 | func resume(client *rpc.Client, resume []string, silent *bool) error { 27 | pipes := &jobsv1.Pipelines{Pipelines: resume} 28 | er := &jobsv1.Empty{} 29 | 30 | err := client.Call(resumeRPC, pipes, er) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | if !*silent { 36 | _ = renderPipelines(os.Stdout, resume).Render() 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func destroy(client *rpc.Client, destroy []string, silent *bool) error { 43 | pipes := &jobsv1.Pipelines{Pipelines: destroy} 44 | resp := &jobsv1.Pipelines{} 45 | 46 | err := client.Call(destroyRPC, pipes, resp) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if !*silent { 52 | _ = renderPipelines(os.Stdout, resp.GetPipelines()).Render() 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func list(client *rpc.Client) error { 59 | resp := &jobsv1.Pipelines{} 60 | er := &jobsv1.Empty{} 61 | 62 | err := client.Call(listRPC, er, resp) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | _ = renderPipelines(os.Stdout, resp.GetPipelines()).Render() 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /internal/cli/stop/command.go: -------------------------------------------------------------------------------- 1 | package stop 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/roadrunner-server/errors" 11 | "github.com/roadrunner-server/roadrunner/v2025/internal/sdnotify" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | const ( 16 | // sync with root.go 17 | pidFileName string = ".pid" 18 | ) 19 | 20 | // NewCommand creates `serve` command. 21 | func NewCommand(silent *bool, force *bool) *cobra.Command { 22 | return &cobra.Command{ 23 | Use: "stop", 24 | Short: "Stop RoadRunner server", 25 | RunE: func(*cobra.Command, []string) error { 26 | const op = errors.Op("rr_stop") 27 | 28 | _, _ = sdnotify.SdNotify(sdnotify.Stopping) 29 | data, err := os.ReadFile(pidFileName) 30 | if err != nil { 31 | return errors.Errorf("%v, to create a .pid file, you must run RR with the following options: './rr serve -p'", err) 32 | } 33 | 34 | pid, err := strconv.Atoi(string(data)) 35 | if err != nil { 36 | return errors.E(op, err) 37 | } 38 | 39 | process, err := os.FindProcess(pid) 40 | if err != nil { 41 | return errors.E(op, err) 42 | } 43 | 44 | if !*silent { 45 | log.Printf("stopping process with PID: %d", pid) 46 | } 47 | 48 | err = process.Signal(syscall.SIGTERM) 49 | if err != nil { 50 | return errors.E(op, err) 51 | } 52 | 53 | if *force { 54 | // RR may lose the signal if we immediately send it 55 | time.Sleep(time.Second) 56 | err = process.Signal(syscall.SIGTERM) 57 | if err != nil { 58 | return errors.E(op, err) 59 | } 60 | } 61 | 62 | return nil 63 | }, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/debug/server_test.go: -------------------------------------------------------------------------------- 1 | package debug_test 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "net" 7 | "net/http" 8 | "strconv" 9 | "testing" 10 | "time" 11 | 12 | "github.com/roadrunner-server/roadrunner/v2025/internal/debug" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestServer_StartingAndStopping(t *testing.T) { 18 | rand.Seed(time.Now().UnixNano()) 19 | 20 | var ( 21 | s = debug.NewServer() 22 | port = strconv.Itoa(rand.Intn(10000) + 10000) //nolint:gosec 23 | ) 24 | 25 | go func() { assert.ErrorIs(t, s.Start(":"+port), http.ErrServerClosed) }() 26 | 27 | defer func() { assert.NoError(t, s.Stop(context.Background())) }() 28 | 29 | for range 100 { // wait for server started state 30 | if l, err := net.Dial("tcp", ":"+port); err != nil { 31 | <-time.After(time.Millisecond) 32 | } else { 33 | _ = l.Close() 34 | 35 | break 36 | } 37 | } 38 | 39 | for _, uri := range []string{ // assert that pprof handlers exists 40 | "http://127.0.0.1:" + port + "/debug/pprof/", 41 | "http://127.0.0.1:" + port + "/debug/pprof/cmdline", 42 | // "http://127.0.0.1:" + port + "/debug/pprof/profile", 43 | "http://127.0.0.1:" + port + "/debug/pprof/symbol", 44 | // "http://127.0.0.1:" + port + "/debug/pprof/trace", 45 | } { 46 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 47 | 48 | req, _ := http.NewRequestWithContext(ctx, http.MethodHead, uri, http.NoBody) 49 | resp, err := http.DefaultClient.Do(req) 50 | assert.NoError(t, err) 51 | assert.Equal(t, http.StatusOK, resp.StatusCode) 52 | 53 | _ = resp.Body.Close() 54 | 55 | cancel() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cmd/rr/command_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_Main(t *testing.T) { 14 | os.Args = []string{"rr", "--help"} 15 | exitFn = func(code int) { assert.Equal(t, 0, code) } 16 | 17 | r, w, _ := os.Pipe() 18 | os.Stdout = w 19 | 20 | main() 21 | _ = w.Close() 22 | buf := new(bytes.Buffer) 23 | 24 | _ = r.SetReadDeadline(time.Now().Add(time.Second)) 25 | _, _ = io.Copy(buf, r) 26 | 27 | assert.Contains(t, buf.String(), "Usage:") 28 | assert.Contains(t, buf.String(), "Available Commands:") 29 | assert.Contains(t, buf.String(), "Flags:") 30 | } 31 | 32 | func Test_MainWithoutCommands(t *testing.T) { 33 | os.Args = []string{"rr"} 34 | exitFn = func(code int) { assert.Equal(t, 0, code) } 35 | 36 | r, w, _ := os.Pipe() 37 | os.Stdout = w 38 | 39 | main() 40 | buf := new(bytes.Buffer) 41 | _ = r.SetReadDeadline(time.Now().Add(time.Second)) 42 | _, _ = io.Copy(buf, r) 43 | 44 | assert.Contains(t, buf.String(), "Usage:") 45 | assert.Contains(t, buf.String(), "Available Commands:") 46 | assert.Contains(t, buf.String(), "Flags:") 47 | } 48 | 49 | func Test_MainUnknownSubcommand(t *testing.T) { 50 | os.Args = []string{"", "foobar"} 51 | exitFn = func(code int) { assert.Equal(t, 1, code) } 52 | 53 | r, w, _ := os.Pipe() 54 | os.Stderr = w 55 | 56 | main() 57 | _ = w.Close() 58 | buf := new(bytes.Buffer) 59 | 60 | _ = r.SetReadDeadline(time.Now().Add(time.Second)) 61 | _, _ = io.Copy(buf, r) 62 | 63 | assert.Contains(t, buf.String(), "unknown command") 64 | assert.Contains(t, buf.String(), "foobar") 65 | } 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: 🐛 File a bug report 3 | title: "[🐛 BUG]: " 4 | labels: ["B-bug", "F-need-verification"] 5 | assignees: 6 | - rustatian 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! 12 | 13 | - type: checkboxes 14 | id: search-done 15 | attributes: 16 | label: No duplicates 🥲. 17 | options: 18 | - label: I have searched for a similar issue in our bug tracker and didn't find any solutions. 19 | required: true 20 | 21 | - type: textarea 22 | id: what-happened 23 | attributes: 24 | label: What happened? 25 | description: Also tell us, what did you expect to happen? 26 | placeholder: Tell us what you see! 27 | value: "A bug happened!" 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: version 33 | attributes: 34 | label: Version (rr --version) 35 | description: What version of our software are you running? 36 | placeholder: 2.6.0 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | id: repro 42 | attributes: 43 | label: How to reproduce the issue? 44 | description: .rr.yaml and steps on how to reproduce the issue? 45 | validations: 46 | required: true 47 | 48 | - type: textarea 49 | id: logs 50 | attributes: 51 | label: Relevant log output 52 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 53 | render: shell 54 | -------------------------------------------------------------------------------- /container/config.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "time" 7 | 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | // Config defines endure container configuration. 12 | type Config struct { 13 | GracePeriod time.Duration `mapstructure:"grace_period"` 14 | LogLevel string `mapstructure:"log_level"` 15 | WatchdogSec int `mapstructure:"watchdog_sec"` 16 | PrintGraph bool `mapstructure:"print_graph"` 17 | } 18 | 19 | const ( 20 | // endure config key 21 | endureKey = "endure" 22 | // overall grace period, after which container will be stopped forcefully 23 | defaultGracePeriod = time.Second * 30 24 | ) 25 | 26 | // NewConfig creates endure container configuration. 27 | func NewConfig(cfgFile string) (*Config, error) { 28 | v := viper.New() 29 | v.SetConfigFile(cfgFile) 30 | 31 | err := v.ReadInConfig() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | cfg := &Config{ 37 | GracePeriod: defaultGracePeriod, 38 | LogLevel: "error", 39 | PrintGraph: false, 40 | } 41 | 42 | if !v.IsSet(endureKey) { 43 | return cfg, nil 44 | } 45 | 46 | err = v.UnmarshalKey(endureKey, cfg) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return cfg, nil 52 | } 53 | 54 | func ParseLogLevel(s string) (slog.Leveler, error) { 55 | switch s { 56 | case "debug": 57 | return slog.LevelDebug, nil 58 | case "info": 59 | return slog.LevelInfo, nil 60 | case "warn", "warning": 61 | return slog.LevelWarn, nil 62 | case "error": 63 | return slog.LevelError, nil 64 | default: 65 | return slog.LevelError, fmt.Errorf(`unknown log level "%s" (allowed: debug, info, warn, error)`, s) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/rpc/includes.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "github.com/roadrunner-server/errors" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | const ( 9 | versionKey string = "version" 10 | includeKey string = "include" 11 | defaultConfigVersion string = "3" 12 | prevConfigVersion string = "2.7" 13 | ) 14 | 15 | func getConfiguration(path string) (map[string]any, string, error) { 16 | v := viper.New() 17 | v.SetConfigFile(path) 18 | err := v.ReadInConfig() 19 | if err != nil { 20 | return nil, "", err 21 | } 22 | 23 | // get configuration version 24 | ver := v.Get(versionKey) 25 | if ver == nil { 26 | return nil, "", errors.Str("rr configuration file should contain a version e.g: version: 2.7") 27 | } 28 | 29 | if _, ok := ver.(string); !ok { 30 | return nil, "", errors.Errorf("type of version should be string, actual: %T", ver) 31 | } 32 | 33 | // automatically inject ENV variables using ${ENV} pattern 34 | expandEnvViper(v) 35 | 36 | return v.AllSettings(), ver.(string), nil 37 | } 38 | 39 | func handleInclude(rootVersion string, v *viper.Viper) error { 40 | // automatically inject ENV variables using ${ENV} pattern 41 | // root config 42 | expandEnvViper(v) 43 | 44 | ifiles := v.GetStringSlice(includeKey) 45 | if ifiles == nil { 46 | return nil 47 | } 48 | 49 | for _, file := range ifiles { 50 | config, version, err := getConfiguration(file) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | if version != rootVersion { 56 | return errors.Str("version in included file must be the same as in root") 57 | } 58 | 59 | // overriding configuration 60 | for key, val := range config { 61 | v.Set(key, val) 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | allow-parallel-runners: true 4 | output: 5 | formats: 6 | text: 7 | path: stdout 8 | linters: 9 | default: none 10 | enable: 11 | - asciicheck 12 | - bodyclose 13 | - copyloopvar 14 | - dogsled 15 | - dupl 16 | - errcheck 17 | - errorlint 18 | - exhaustive 19 | - gochecknoglobals 20 | - goconst 21 | - gocritic 22 | - goprintffuncname 23 | - gosec 24 | - govet 25 | - ineffassign 26 | - misspell 27 | - nakedret 28 | - noctx 29 | - nolintlint 30 | - prealloc 31 | - revive 32 | - staticcheck 33 | - tparallel 34 | - unconvert 35 | - unparam 36 | - unused 37 | - whitespace 38 | settings: 39 | dupl: 40 | threshold: 100 41 | goconst: 42 | min-len: 2 43 | min-occurrences: 3 44 | godot: 45 | scope: declarations 46 | capital: true 47 | lll: 48 | line-length: 120 49 | misspell: 50 | locale: US 51 | nolintlint: 52 | require-specific: true 53 | prealloc: 54 | simple: true 55 | range-loops: true 56 | for-loops: true 57 | wsl: 58 | allow-assign-and-anything: true 59 | exclusions: 60 | generated: lax 61 | presets: 62 | - comments 63 | - common-false-positives 64 | - legacy 65 | - std-error-handling 66 | rules: 67 | - linters: 68 | - dupl 69 | - funlen 70 | - gocognit 71 | - scopelint 72 | path: _test\.go 73 | paths: 74 | - internal/debug/server_test.go 75 | - .github 76 | - .git 77 | - third_party$ 78 | - builtin$ 79 | - examples$ 80 | formatters: 81 | enable: 82 | - gofmt 83 | - goimports 84 | exclusions: 85 | generated: lax 86 | paths: 87 | - internal/debug/server_test.go 88 | - .github 89 | - .git 90 | - third_party$ 91 | - builtin$ 92 | - examples$ 93 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Image page: 2 | FROM --platform=${TARGETPLATFORM:-linux/amd64} golang:1.25-alpine AS builder 3 | 4 | # app version and build date must be passed during image building (version without any prefix). 5 | # e.g.: `docker build --build-arg "APP_VERSION=1.2.3" --build-arg "BUILD_TIME=$(date +%FT%T%z)" .` 6 | ARG APP_VERSION="undefined" 7 | ARG BUILD_TIME="undefined" 8 | 9 | COPY . /src 10 | 11 | WORKDIR /src 12 | 13 | # arguments to pass on each go tool link invocation 14 | ENV LDFLAGS="-s \ 15 | -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.version=$APP_VERSION \ 16 | -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.buildTime=$BUILD_TIME" 17 | 18 | # compile binary file 19 | RUN set -x 20 | RUN go mod download 21 | RUN go mod tidy 22 | RUN CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o ./rr ./cmd/rr 23 | RUN ./rr -v 24 | 25 | FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:3 26 | 27 | RUN apk upgrade --update-cache --available && \ 28 | apk add openssl && \ 29 | rm -rf /var/cache/apk/* 30 | 31 | # use same build arguments for image labels 32 | ARG APP_VERSION="undefined" 33 | ARG BUILD_TIME="undefined" 34 | 35 | # https://github.com/opencontainers/image-spec/blob/main/annotations.md 36 | LABEL org.opencontainers.image.title="roadrunner" 37 | LABEL org.opencontainers.image.description="High-performance PHP application server and process manager written in Go and powered with plugins" 38 | LABEL org.opencontainers.image.url="https://roadrunner.dev" 39 | LABEL org.opencontainers.image.source="https://github.com/roadrunner-server/roadrunner" 40 | LABEL org.opencontainers.image.vendor="SpiralScout" 41 | LABEL org.opencontainers.image.version="$APP_VERSION" 42 | LABEL org.opencontainers.image.created="$BUILD_TIME" 43 | LABEL org.opencontainers.image.licenses="MIT" 44 | 45 | # copy required files from builder image 46 | COPY --from=builder /src/rr /usr/bin/rr 47 | COPY --from=builder /src/.rr.yaml /etc/rr.yaml 48 | 49 | # use roadrunner binary as image entrypoint 50 | ENTRYPOINT ["/usr/bin/rr"] 51 | -------------------------------------------------------------------------------- /internal/cli/reset/command.go: -------------------------------------------------------------------------------- 1 | package reset 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | 7 | internalRpc "github.com/roadrunner-server/roadrunner/v2025/internal/rpc" 8 | "github.com/roadrunner-server/roadrunner/v2025/internal/sdnotify" 9 | 10 | "github.com/roadrunner-server/errors" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | const ( 15 | op = errors.Op("reset_handler") 16 | resetterList = "resetter.List" 17 | resetterReset = "resetter.Reset" 18 | ) 19 | 20 | // NewCommand creates `reset` command. 21 | func NewCommand(cfgFile *string, override *[]string, silent *bool) *cobra.Command { 22 | return &cobra.Command{ 23 | Use: "reset", 24 | Short: "Reset workers of all or specific RoadRunner service", 25 | RunE: func(_ *cobra.Command, args []string) error { 26 | if cfgFile == nil { 27 | return errors.E(op, errors.Str("no configuration file provided")) 28 | } 29 | 30 | client, err := internalRpc.NewClient(*cfgFile, *override) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | defer func() { _ = client.Close() }() 36 | 37 | plugins := args // by default, we expect services list from user 38 | if len(plugins) == 0 { // but if nothing was passed - request all services list 39 | if err = client.Call(resetterList, true, &plugins); err != nil { 40 | return err 41 | } 42 | } 43 | 44 | _, _ = sdnotify.SdNotify(sdnotify.Reloading) 45 | 46 | var wg sync.WaitGroup 47 | wg.Add(len(plugins)) 48 | 49 | for _, plugin := range plugins { 50 | // simulating some work 51 | go func(p string) { 52 | if !*silent { 53 | log.Printf("resetting plugin: [%s] ", p) 54 | } 55 | defer wg.Done() 56 | 57 | var done bool 58 | <-client.Go(resetterReset, p, &done, nil).Done 59 | 60 | if err != nil { 61 | log.Println(err) 62 | 63 | return 64 | } 65 | 66 | if !*silent { 67 | log.Printf("plugin reset: [%s]", p) 68 | } 69 | }(plugin) 70 | } 71 | 72 | wg.Wait() 73 | 74 | _, _ = sdnotify.SdNotify(sdnotify.Ready) 75 | 76 | return nil 77 | }, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/roadrunner_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/roadrunner-server/informer/v5" 9 | "github.com/roadrunner-server/resetter/v5" 10 | "github.com/roadrunner-server/roadrunner/v2025/lib" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestNewFailsOnMissingConfig(t *testing.T) { 15 | _, err := lib.NewRR("config/file/does/not/exist/.rr.yaml", []string{}, lib.DefaultPluginsList()) 16 | assert.NotNil(t, err) 17 | } 18 | 19 | const testConfigWithVersion = ` 20 | version: '3' 21 | server: 22 | command: "php src/index.php" 23 | relay: "pipes" 24 | 25 | endure: 26 | grace_period: 1s 27 | ` 28 | 29 | const testConfig = ` 30 | server: 31 | command: "php src/index.php" 32 | relay: "pipes" 33 | 34 | endure: 35 | grace_period: 1s 36 | ` 37 | 38 | func makeConfig(t *testing.T, configYaml string) string { 39 | cfgFile := os.TempDir() + "/.rr.yaml" 40 | err := os.WriteFile(cfgFile, []byte(configYaml), 0600) 41 | assert.NoError(t, err) 42 | 43 | return cfgFile 44 | } 45 | 46 | func TestNewWithOldConfig(t *testing.T) { 47 | cfgFile := makeConfig(t, testConfig) 48 | _, err := lib.NewRR(cfgFile, []string{}, lib.DefaultPluginsList()) 49 | assert.Error(t, err) 50 | 51 | t.Cleanup(func() { 52 | _ = os.Remove(cfgFile) 53 | }) 54 | } 55 | 56 | func TestNewWithConfig(t *testing.T) { 57 | cfgFile := makeConfig(t, testConfigWithVersion) 58 | rr, err := lib.NewRR(cfgFile, []string{}, lib.DefaultPluginsList()) 59 | assert.NoError(t, err) 60 | assert.Equal(t, "3", rr.Version) 61 | 62 | t.Cleanup(func() { 63 | _ = os.Remove(cfgFile) 64 | }) 65 | } 66 | 67 | func TestServeStop(t *testing.T) { 68 | cfgFile := makeConfig(t, testConfigWithVersion) 69 | plugins := []any{ 70 | &informer.Plugin{}, 71 | &resetter.Plugin{}, 72 | } 73 | rr, err := lib.NewRR(cfgFile, []string{}, plugins) 74 | assert.NoError(t, err) 75 | 76 | errchan := make(chan error, 1) 77 | stopchan := make(chan struct{}, 1) 78 | 79 | go func() { 80 | errchan <- rr.Serve() 81 | stopchan <- struct{}{} 82 | }() 83 | 84 | rr.Stop() 85 | time.Sleep(time.Second * 2) 86 | 87 | assert.Equal(t, struct{}{}, <-stopchan) 88 | assert.Nil(t, <-errchan) 89 | 90 | t.Cleanup(func() { 91 | _ = os.Remove(cfgFile) 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /container/config_test.go: -------------------------------------------------------------------------------- 1 | package container_test 2 | 3 | import ( 4 | "log/slog" 5 | "testing" 6 | "time" 7 | 8 | "github.com/roadrunner-server/config/v5" 9 | "github.com/roadrunner-server/roadrunner/v2025/container" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestNewConfig_SuccessfulReading(t *testing.T) { 14 | c, err := container.NewConfig("test/endure_ok.yaml") 15 | assert.NoError(t, err) 16 | assert.NotNil(t, c) 17 | 18 | ll, err := container.ParseLogLevel(c.LogLevel) 19 | assert.NoError(t, err) 20 | 21 | assert.Equal(t, time.Second*10, c.GracePeriod) 22 | assert.True(t, c.PrintGraph) 23 | assert.Equal(t, slog.LevelWarn, ll.Level()) 24 | } 25 | 26 | func TestNewConfig_WithoutEndureKey(t *testing.T) { 27 | cfgPlugin := &config.Plugin{Type: "yaml", ReadInCfg: []byte{}} 28 | assert.NoError(t, cfgPlugin.Init()) 29 | 30 | c, err := container.NewConfig("test/without_endure_ok.yaml") 31 | assert.NoError(t, err) 32 | assert.NotNil(t, c) 33 | 34 | ll, err := container.ParseLogLevel(c.LogLevel) 35 | assert.NoError(t, err) 36 | 37 | assert.Equal(t, time.Second*30, c.GracePeriod) 38 | assert.False(t, c.PrintGraph) 39 | assert.Equal(t, slog.LevelError, ll.Level()) 40 | } 41 | 42 | func TestNewConfig_LoggingLevels(t *testing.T) { 43 | for _, tt := range []struct { 44 | path string 45 | giveLevel string 46 | wantLevel slog.Leveler 47 | wantError bool 48 | }{ 49 | {path: "test/endure_ok_debug.yaml", giveLevel: "debug", wantLevel: slog.LevelDebug}, 50 | {path: "test/endure_ok_info.yaml", giveLevel: "info", wantLevel: slog.LevelInfo}, 51 | {path: "test/endure_ok_warn.yaml", giveLevel: "warn", wantLevel: slog.LevelWarn}, 52 | {path: "test/endure_ok_error.yaml", giveLevel: "error", wantLevel: slog.LevelError}, 53 | 54 | {path: "test/endure_ok_foobar.yaml", giveLevel: "foobar", wantError: true}, 55 | } { 56 | t.Run(tt.giveLevel, func(t *testing.T) { 57 | cfgPlugin := &config.Plugin{Type: "yaml", ReadInCfg: []byte("endure:\n log_level: " + tt.giveLevel)} 58 | assert.NoError(t, cfgPlugin.Init()) 59 | 60 | c, err := container.NewConfig(tt.path) 61 | assert.NotNil(t, c) 62 | ll, err2 := container.ParseLogLevel(c.LogLevel) 63 | 64 | if tt.wantError { 65 | assert.Error(t, err2) 66 | assert.Contains(t, err2.Error(), "unknown log level") 67 | } else { 68 | assert.NoError(t, err) 69 | assert.NoError(t, err2) 70 | assert.Equal(t, tt.wantLevel, ll.Level()) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /benchmarks/simple.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { sleep } from 'k6'; 3 | 4 | export const options = { 5 | // A number specifying the number of VUs to run concurrently. 6 | vus: 1000, 7 | // A string specifying the total duration of the test run. 8 | duration: '30s', 9 | 10 | // The following section contains configuration options for execution of this 11 | // test script in Grafana Cloud. 12 | // 13 | // See https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/ 14 | // to learn about authoring and running k6 test scripts in Grafana k6 Cloud. 15 | // 16 | // ext: { 17 | // loadimpact: { 18 | // // The ID of the project to which the test is assigned in the k6 Cloud UI. 19 | // // By default tests are executed in default project. 20 | // projectID: "", 21 | // // The name of the test in the k6 Cloud UI. 22 | // // Test runs with the same name will be grouped. 23 | // name: "script.js" 24 | // } 25 | // }, 26 | 27 | // Uncomment this section to enable the use of Browser API in your tests. 28 | // 29 | // See https://grafana.com/docs/k6/latest/using-k6-browser/running-browser-tests/ to learn more 30 | // about using Browser API in your test scripts. 31 | // 32 | // scenarios: { 33 | // // The scenario name appears in the result summary, tags, and so on. 34 | // // You can give the scenario any name, as long as each name in the script is unique. 35 | // ui: { 36 | // // Executor is a mandatory parameter for browser-based tests. 37 | // // Shared iterations in this case tells k6 to reuse VUs to execute iterations. 38 | // // 39 | // // See https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ for other executor types. 40 | // executor: 'shared-iterations', 41 | // options: { 42 | // browser: { 43 | // // This is a mandatory parameter that instructs k6 to launch and 44 | // // connect to a chromium-based browser, and use it to run UI-based 45 | // // tests. 46 | // type: 'chromium', 47 | // }, 48 | // }, 49 | // }, 50 | // } 51 | }; 52 | 53 | // The function that defines VU logic. 54 | // 55 | // See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more 56 | // about authoring k6 scripts. 57 | // 58 | let params = { 59 | timeout: '120s' 60 | }; 61 | export default function() { 62 | http.get('http://127.0.0.1:15389', params); 63 | } 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to RoadRunner docs contributing guide 2 | 3 | Thank you for investing your time in contributing to our project! Any contribution you make will be reflected on [RR](https://github.com/roadrunner-server/roadrunner#contributors) :sparkles:. 4 | 5 | Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | ### Issues 10 | 11 | #### Create a new issue 12 | 13 | If you spot a problem with the RR, [search if an issue already exists](https://github.com/roadrunner-server/roadrunner/issues). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/roadrunner-server/roadrunner/issues/new/choose). 14 | 15 | #### Solve an issue 16 | 17 | Scan through our [existing issues](https://github.com/roadrunner-server/roadrunner/issues) to find one that interests you. You can narrow down the search using `labels` as filters. If you find an issue to work on, you are welcome to open a PR with a fix. 18 | 19 | ### Pull Request 20 | 21 | When you're finished with the changes, create a pull request, also known as a PR. 22 | - Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request. 23 | - Don't forget to update the docs if you are solving one. 24 | - Make sure, that all checkboxes in the PR template are solved. 25 | Once you submit your PR, a RR team member will review your proposal. We may ask questions or request for additional information. 26 | - As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations). 27 | - If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues. 28 | 29 | ### Your PR is merged! 30 | 31 | Congratulations :tada::tada: The RoadRunner team thanks you :sparkles:. 32 | 33 | Once your PR is merged, your contributions will be publicly visible on the [RR page](https://github.com/roadrunner-server/roadrunner#contributors). 34 | 35 | Now that you are part of the RoadRunner server community, see how else you can [contribute to the RR](https://github.com/roadrunner-server/roadrunner/issues). 36 | -------------------------------------------------------------------------------- /internal/cli/jobs/command.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "strings" 5 | 6 | internalRpc "github.com/roadrunner-server/roadrunner/v2025/internal/rpc" 7 | 8 | "github.com/roadrunner-server/errors" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | const ( 13 | listRPC string = "jobs.List" 14 | pauseRPC string = "jobs.Pause" 15 | destroyRPC string = "jobs.Destroy" 16 | resumeRPC string = "jobs.Resume" 17 | ) 18 | 19 | // NewCommand creates `jobs` command. 20 | func NewCommand(cfgFile *string, override *[]string, silent *bool) *cobra.Command { 21 | var ( 22 | pausePipes bool 23 | destroyPipes bool 24 | resumePipes bool 25 | listPipes bool 26 | ) 27 | 28 | cmd := &cobra.Command{ 29 | Use: "jobs", 30 | Short: "Jobs pipelines manipulation", 31 | RunE: func(_ *cobra.Command, args []string) error { 32 | const op = errors.Op("jobs_command") 33 | 34 | if cfgFile == nil { 35 | return errors.E(op, errors.Str("no configuration file provided")) 36 | } 37 | 38 | client, err := internalRpc.NewClient(*cfgFile, *override) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | defer func() { _ = client.Close() }() 44 | 45 | switch { 46 | case pausePipes: 47 | if len(args) == 0 { 48 | return errors.Str("incorrect command usage, should be: rr jobs --pause pipe1,pipe2") 49 | } 50 | split := strings.Split(strings.Trim(args[0], " "), ",") 51 | 52 | return pause(client, split, silent) 53 | case destroyPipes: 54 | if len(args) == 0 { 55 | return errors.Str("incorrect command usage, should be: rr jobs --destroy pipe1,pipe2") 56 | } 57 | split := strings.Split(strings.Trim(args[0], " "), ",") 58 | 59 | return destroy(client, split, silent) 60 | case resumePipes: 61 | if len(args) == 0 { 62 | return errors.Str("incorrect command usage, should be: rr jobs --resume pipe1,pipe2") 63 | } 64 | split := strings.Split(strings.Trim(args[0], " "), ",") 65 | 66 | return resume(client, split, silent) 67 | case listPipes: 68 | return list(client) 69 | default: 70 | return errors.Str("command should be in form of: `rr jobs -- pipe1,pipe2`") 71 | } 72 | }, 73 | } 74 | 75 | // commands 76 | cmd.Flags().BoolVar(&pausePipes, "pause", false, "pause pipelines") 77 | cmd.Flags().BoolVar(&destroyPipes, "destroy", false, "destroy pipelines") 78 | cmd.Flags().BoolVar(&resumePipes, "resume", false, "resume pipelines") 79 | cmd.Flags().BoolVar(&listPipes, "list", false, "list pipelines") 80 | 81 | return cmd 82 | } 83 | -------------------------------------------------------------------------------- /internal/rpc/client_test.go: -------------------------------------------------------------------------------- 1 | package rpc_test 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "testing" 7 | 8 | "github.com/roadrunner-server/config/v5" 9 | "github.com/roadrunner-server/roadrunner/v2025/internal/rpc" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestNewClient_RpcServiceDisabled(t *testing.T) { 16 | cfgPlugin := &config.Plugin{Type: "yaml", ReadInCfg: []byte{}} 17 | assert.NoError(t, cfgPlugin.Init()) 18 | 19 | c, err := rpc.NewClient("test/config_rpc_empty.yaml", nil) 20 | 21 | assert.Nil(t, c) 22 | assert.EqualError(t, err, "rpc service not specified in the configuration. Tip: add\n rpc:\n\r listen: rr_rpc_address") 23 | } 24 | 25 | func TestNewClient_WrongRcpConfiguration(t *testing.T) { 26 | c, err := rpc.NewClient("test/config_rpc_wrong.yaml", nil) 27 | 28 | assert.Nil(t, c) 29 | assert.Error(t, err) 30 | assert.Contains(t, err.Error(), "invalid socket DSN") 31 | } 32 | 33 | func TestNewClient_ConnectionError(t *testing.T) { 34 | c, err := rpc.NewClient("test/config_rpc_conn_err.yaml", nil) 35 | 36 | assert.Nil(t, c) 37 | assert.Error(t, err) 38 | assert.Contains(t, err.Error(), "connection refused") 39 | } 40 | 41 | func TestNewClient_SuccessfullyConnected(t *testing.T) { 42 | l, err := net.Listen("tcp", "127.0.0.1:55554") //nolint:noctx 43 | assert.NoError(t, err) 44 | 45 | defer func() { assert.NoError(t, l.Close()) }() 46 | 47 | c, err := rpc.NewClient("test/config_rpc_ok.yaml", nil) 48 | 49 | assert.NotNil(t, c) 50 | assert.NoError(t, err) 51 | 52 | defer func() { assert.NoError(t, c.Close()) }() 53 | } 54 | 55 | func TestNewClient_WithIncludes(t *testing.T) { 56 | l, err := net.Listen("tcp", "127.0.0.1:6010") //nolint:noctx 57 | assert.NoError(t, err) 58 | 59 | defer func() { assert.NoError(t, l.Close()) }() 60 | 61 | c, err := rpc.NewClient("test/include1/.rr.yaml", nil) 62 | 63 | assert.NotNil(t, c) 64 | assert.NoError(t, err) 65 | 66 | assert.NoError(t, c.Close()) 67 | } 68 | 69 | func TestNewClient_SuccessfullyConnectedOverride(t *testing.T) { 70 | l, err := net.Listen("tcp", "127.0.0.1:55554") //nolint:noctx 71 | assert.NoError(t, err) 72 | 73 | defer func() { assert.NoError(t, l.Close()) }() 74 | 75 | c, err := rpc.NewClient("test/config_rpc_empty.yaml", []string{"rpc.listen=tcp://127.0.0.1:55554"}) 76 | 77 | assert.NotNil(t, c) 78 | assert.NoError(t, err) 79 | 80 | defer func() { assert.NoError(t, c.Close()) }() 81 | } 82 | 83 | // ${} syntax 84 | func TestNewClient_SuccessfullyConnectedEnvDollarSyntax(t *testing.T) { 85 | l, err := net.Listen("tcp", "127.0.0.1:55556") //nolint:noctx 86 | assert.NoError(t, err) 87 | 88 | defer func() { assert.NoError(t, l.Close()) }() 89 | 90 | require.NoError(t, os.Setenv("RPC", "tcp://127.0.0.1:55556")) 91 | c, err := rpc.NewClient("test/config_rpc_ok_env.yaml", nil) 92 | 93 | assert.NotNil(t, c) 94 | assert.NoError(t, err) 95 | 96 | defer func() { assert.NoError(t, c.Close()) }() 97 | } 98 | -------------------------------------------------------------------------------- /lib/roadrunner.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | 7 | configImpl "github.com/roadrunner-server/config/v5" 8 | "github.com/roadrunner-server/endure/v2" 9 | "github.com/roadrunner-server/roadrunner/v2025/container" 10 | ) 11 | 12 | const ( 13 | rrModule string = "github.com/roadrunner-server/roadrunner/v2025" 14 | ) 15 | 16 | type RR struct { 17 | container *endure.Endure 18 | stop chan struct{} 19 | Version string 20 | } 21 | 22 | // NewRR creates a new RR instance that can then be started or stopped by the caller 23 | func NewRR(cfgFile string, override []string, pluginList []any) (*RR, error) { 24 | // create endure container config 25 | containerCfg, err := container.NewConfig(cfgFile) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | cfg := &configImpl.Plugin{ 31 | Path: cfgFile, 32 | Timeout: containerCfg.GracePeriod, 33 | Flags: override, 34 | Version: getRRVersion(), 35 | } 36 | 37 | // create endure container 38 | endureOptions := []endure.Options{ 39 | endure.GracefulShutdownTimeout(containerCfg.GracePeriod), 40 | } 41 | 42 | if containerCfg.PrintGraph { 43 | endureOptions = append(endureOptions, endure.Visualize()) 44 | } 45 | 46 | // create endure container 47 | ll, err := container.ParseLogLevel(containerCfg.LogLevel) 48 | if err != nil { 49 | return nil, err 50 | } 51 | endureContainer := endure.New(ll, endureOptions...) 52 | 53 | // register another container plugin 54 | err = endureContainer.RegisterAll(append(pluginList, cfg)...) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | // init container and all services 60 | err = endureContainer.Init() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return &RR{ 66 | container: endureContainer, 67 | stop: make(chan struct{}, 1), 68 | Version: cfg.Version, 69 | }, nil 70 | } 71 | 72 | // Serve starts RR and starts listening for requests. 73 | // This is a blocking call that will return an error if / when one occurs in a plugin 74 | func (rr *RR) Serve() error { 75 | // start serving the graph 76 | errCh, err := rr.container.Serve() 77 | if err != nil { 78 | return err 79 | } 80 | 81 | select { 82 | case e := <-errCh: 83 | return fmt.Errorf("error: %w\nplugin: %s", e.Error, e.VertexID) 84 | case <-rr.stop: 85 | return rr.container.Stop() 86 | } 87 | } 88 | 89 | func (rr *RR) Plugins() []string { 90 | return rr.container.Plugins() 91 | } 92 | 93 | // Stop stops roadrunner 94 | func (rr *RR) Stop() { 95 | rr.stop <- struct{}{} 96 | } 97 | 98 | // DefaultPluginsList returns all the plugins that RR can run with and are included by default 99 | func DefaultPluginsList() []any { 100 | return container.Plugins() 101 | } 102 | 103 | // Tries to find the version info for a given module's path 104 | // empty string if not found 105 | func getRRVersion() string { 106 | bi, ok := debug.ReadBuildInfo() 107 | if !ok { 108 | return "" 109 | } 110 | 111 | for i := range bi.Deps { 112 | if bi.Deps[i].Path == rrModule { 113 | return bi.Deps[i].Version 114 | } 115 | } 116 | 117 | return "" 118 | } 119 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at wolfy-j@spiralscout.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /container/plugins.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "github.com/roadrunner-server/amqp/v5" 5 | appLogger "github.com/roadrunner-server/app-logger/v5" 6 | "github.com/roadrunner-server/beanstalk/v5" 7 | "github.com/roadrunner-server/boltdb/v5" 8 | "github.com/roadrunner-server/centrifuge/v5" 9 | "github.com/roadrunner-server/fileserver/v5" 10 | gps "github.com/roadrunner-server/google-pub-sub/v5" 11 | grpcPlugin "github.com/roadrunner-server/grpc/v5" 12 | "github.com/roadrunner-server/gzip/v5" 13 | "github.com/roadrunner-server/headers/v5" 14 | httpPlugin "github.com/roadrunner-server/http/v5" 15 | "github.com/roadrunner-server/informer/v5" 16 | "github.com/roadrunner-server/jobs/v5" 17 | "github.com/roadrunner-server/kafka/v5" 18 | "github.com/roadrunner-server/kv/v5" 19 | "github.com/roadrunner-server/lock/v5" 20 | "github.com/roadrunner-server/logger/v5" 21 | "github.com/roadrunner-server/memcached/v5" 22 | "github.com/roadrunner-server/memory/v5" 23 | "github.com/roadrunner-server/metrics/v5" 24 | "github.com/roadrunner-server/nats/v5" 25 | rrOtel "github.com/roadrunner-server/otel/v5" 26 | "github.com/roadrunner-server/prometheus/v5" 27 | proxyIP "github.com/roadrunner-server/proxy_ip_parser/v5" 28 | "github.com/roadrunner-server/redis/v5" 29 | "github.com/roadrunner-server/resetter/v5" 30 | rpcPlugin "github.com/roadrunner-server/rpc/v5" 31 | "github.com/roadrunner-server/send/v5" 32 | "github.com/roadrunner-server/server/v5" 33 | "github.com/roadrunner-server/service/v5" 34 | "github.com/roadrunner-server/sqs/v5" 35 | "github.com/roadrunner-server/static/v5" 36 | "github.com/roadrunner-server/status/v5" 37 | "github.com/roadrunner-server/tcp/v5" 38 | rrt "github.com/temporalio/roadrunner-temporal/v5" 39 | ) 40 | 41 | // Plugins return active plugins for the endured container. Feel free to add or remove any plugins. 42 | func Plugins() []any { //nolint:funlen 43 | return []any{ 44 | // bundled 45 | // informer plugin (./rr workers, ./rr workers -i) 46 | &informer.Plugin{}, 47 | // resetter plugin (./rr reset) 48 | &resetter.Plugin{}, 49 | // mutexes(locks) 50 | &lock.Plugin{}, 51 | // logger plugin 52 | &logger.Plugin{}, 53 | // psr-3 logger extension 54 | &appLogger.Plugin{}, 55 | // metrics plugin 56 | &metrics.Plugin{}, 57 | // rpc plugin (workers, reset) 58 | &rpcPlugin.Plugin{}, 59 | // server plugin (NewWorker, NewWorkerPool) 60 | &server.Plugin{}, 61 | // service plugin 62 | &service.Plugin{}, 63 | // centrifuge 64 | ¢rifuge.Plugin{}, 65 | // 66 | // ========= JOBS bundle 67 | &jobs.Plugin{}, 68 | &amqp.Plugin{}, 69 | &sqs.Plugin{}, 70 | &nats.Plugin{}, 71 | &kafka.Plugin{}, 72 | &beanstalk.Plugin{}, 73 | &gps.Plugin{}, 74 | // ========= 75 | // 76 | // http server plugin with middleware 77 | &httpPlugin.Plugin{}, 78 | &static.Plugin{}, 79 | &headers.Plugin{}, 80 | &status.Plugin{}, 81 | &gzip.Plugin{}, 82 | &prometheus.Plugin{}, 83 | &send.Plugin{}, 84 | &proxyIP.Plugin{}, 85 | &rrOtel.Plugin{}, 86 | &fileserver.Plugin{}, 87 | // =================== 88 | // gRPC 89 | &grpcPlugin.Plugin{}, 90 | // =================== 91 | // KV + Jobs 92 | &memory.Plugin{}, 93 | // KV + Jobs 94 | &boltdb.Plugin{}, 95 | // ============== KV 96 | &kv.Plugin{}, 97 | &memcached.Plugin{}, 98 | &redis.Plugin{}, 99 | // ============== 100 | // raw TCP connections handling 101 | &tcp.Plugin{}, 102 | // temporal plugin 103 | &rrt.Plugin{}, 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/sdnotify/sdnotify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Docker, Inc. 2 | // Copyright 2015-2018 CoreOS, Inc. 3 | // Copyright 2025 SpiralScout. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | // Package sdnotify provides a Go implementation of the sd_notify protocol. 19 | // It can be used to inform systemd of service start-up completion, watchdog 20 | // events, and other status changes. 21 | // 22 | // https://www.freedesktop.org/software/systemd/man/sd_notify.html#Description 23 | package sdnotify 24 | 25 | import ( 26 | "net" 27 | "os" 28 | "time" 29 | ) 30 | 31 | type State string 32 | 33 | const ( 34 | // Ready tells the service manager that service startup is finished 35 | // or the service finished loading its configuration. 36 | // https://www.freedesktop.org/software/systemd/man/sd_notify.html#READY=1 37 | Ready State = "READY=1" 38 | 39 | // Stopping tells the service manager that the service is beginning 40 | // its shutdown. 41 | Stopping State = "STOPPING=1" 42 | 43 | // Reloading tells the service manager that this service is 44 | // reloading its configuration. Note that you must call SdNotifyReady when 45 | // it completed reloading. 46 | Reloading State = "RELOADING=1" 47 | 48 | // Watchdog tells the service manager to update the watchdog 49 | // timestamp for the service. 50 | Watchdog = "WATCHDOG=1" 51 | ) 52 | 53 | // SdNotify sends a message to the init daemon. It is common to ignore the error. 54 | // If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET` 55 | // will be unconditionally unset. 56 | // 57 | // It returns one of the following: 58 | // (false, nil) - notification not supported (i.e. NOTIFY_SOCKET is unset) 59 | // (false, err) - notification supported, but failure happened (e.g. error connecting to NOTIFY_SOCKET or while sending data) 60 | // (true, nil) - notification supported, data has been sent 61 | func SdNotify(state State) (bool, error) { 62 | socketAddr := &net.UnixAddr{ 63 | Name: os.Getenv("NOTIFY_SOCKET"), 64 | Net: "unixgram", 65 | } 66 | 67 | // NOTIFY_SOCKET not set 68 | if socketAddr.Name == "" { 69 | return false, nil 70 | } 71 | 72 | conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) 73 | // Error connecting to NOTIFY_SOCKET 74 | if err != nil { 75 | return false, err 76 | } 77 | 78 | defer func() { 79 | _ = conn.Close() 80 | }() 81 | 82 | if _, err = conn.Write([]byte(state)); err != nil { 83 | return false, err 84 | } 85 | 86 | return true, nil 87 | } 88 | 89 | func StartWatchdog(interval int, stopCh <-chan struct{}) { 90 | go func() { 91 | ticker := time.NewTicker(time.Duration(interval) * time.Second) 92 | defer ticker.Stop() 93 | 94 | for { 95 | select { 96 | case <-stopCh: 97 | return 98 | case <-ticker.C: 99 | supported, err := SdNotify(Watchdog) 100 | if err != nil { 101 | return 102 | } 103 | // notification not supported, stop 104 | if !supported { 105 | return 106 | } 107 | // notification supported, data has been sent, continue 108 | if supported { 109 | continue 110 | } 111 | } 112 | } 113 | }() 114 | } 115 | -------------------------------------------------------------------------------- /internal/cli/workers/command.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | import ( 4 | "fmt" 5 | "net/rpc" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/roadrunner-server/api/v4/plugins/v4/jobs" 12 | internalRpc "github.com/roadrunner-server/roadrunner/v2025/internal/rpc" 13 | 14 | tm "github.com/buger/goterm" 15 | "github.com/fatih/color" 16 | "github.com/roadrunner-server/errors" 17 | "github.com/roadrunner-server/informer/v5" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // NewCommand creates `workers` command. 22 | func NewCommand(cfgFile *string, override *[]string) *cobra.Command { //nolint:funlen 23 | // interactive workers updates 24 | var interactive bool 25 | 26 | cmd := &cobra.Command{ 27 | Use: "workers", 28 | Short: "Show information about active RoadRunner workers", 29 | RunE: func(_ *cobra.Command, args []string) error { 30 | const ( 31 | op = errors.Op("handle_workers_command") 32 | informerList = "informer.List" 33 | ) 34 | 35 | if cfgFile == nil { 36 | return errors.E(op, errors.Str("no configuration file provided")) 37 | } 38 | 39 | client, err := internalRpc.NewClient(*cfgFile, *override) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | defer func() { _ = client.Close() }() 45 | 46 | plugins := args // by default, we expect a plugin list from user 47 | if len(plugins) == 0 { // but if nothing was passed - request all informers list 48 | if err = client.Call(informerList, true, &plugins); err != nil { 49 | return fmt.Errorf("failed to get list of plugins: %w", err) 50 | } 51 | } 52 | 53 | if !interactive { 54 | showWorkers(plugins, client) 55 | return nil 56 | } 57 | 58 | oss := make(chan os.Signal, 1) 59 | signal.Notify(oss, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 60 | 61 | tm.Clear() 62 | 63 | tt := time.NewTicker(time.Second) 64 | defer tt.Stop() 65 | 66 | for { 67 | select { 68 | case <-oss: 69 | return nil 70 | 71 | case <-tt.C: 72 | tm.MoveCursor(1, 1) 73 | tm.Flush() 74 | 75 | showWorkers(plugins, client) 76 | } 77 | } 78 | }, 79 | } 80 | 81 | cmd.Flags().BoolVarP( 82 | &interactive, 83 | "interactive", 84 | "i", 85 | false, 86 | "render interactive workers table", 87 | ) 88 | 89 | return cmd 90 | } 91 | 92 | func showWorkers(plugins []string, client *rpc.Client) { 93 | const ( 94 | informerWorkers = "informer.Workers" 95 | informerJobs = "informer.Jobs" 96 | // this is only one exception to Render the workers, service plugin has the same workers as other plugins, 97 | // but they are RAW processes and needs to be handled in a different way. We don't need a special RPC call, but 98 | // need a special render method. 99 | servicePluginName = "service" 100 | ) 101 | 102 | for _, plugin := range plugins { 103 | list := &informer.WorkerList{} 104 | 105 | if err := client.Call(informerWorkers, plugin, &list); err != nil { 106 | // this is a special case, when we can't get workers list, we need to render an error message 107 | _ = WorkerTable(os.Stdout, list.Workers, fmt.Errorf("failed to receive information about %s plugin: %w", plugin, err)).Render() 108 | continue 109 | } 110 | 111 | if len(list.Workers) == 0 { 112 | continue 113 | } 114 | 115 | if plugin == servicePluginName { 116 | fmt.Printf("Workers of [%s]:\n", color.HiYellowString(plugin)) 117 | _ = ServiceWorkerTable(os.Stdout, list.Workers).Render() 118 | 119 | continue 120 | } 121 | 122 | fmt.Printf("Workers of [%s]:\n", color.HiYellowString(plugin)) 123 | 124 | _ = WorkerTable(os.Stdout, list.Workers, nil).Render() 125 | } 126 | 127 | for _, plugin := range plugins { 128 | var jst []*jobs.State 129 | 130 | if err := client.Call(informerJobs, plugin, &jst); err != nil { 131 | _ = JobsTable(os.Stdout, jst, fmt.Errorf("failed to receive information about %s plugin: %w", plugin, err)).Render() 132 | continue 133 | } 134 | 135 | // eq to nil 136 | if len(jst) == 0 { 137 | continue 138 | } 139 | 140 | fmt.Printf("Jobs of [%s]:\n", color.HiYellowString(plugin)) 141 | _ = JobsTable(os.Stdout, jst, nil).Render() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /schemas/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roadrunner-schema-tests", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "roadrunner-schema-tests", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@apidevtools/json-schema-ref-parser": "^11.7.2", 13 | "ajv": "^8.17.1", 14 | "js-yaml": "^4.1.1" 15 | } 16 | }, 17 | "node_modules/@apidevtools/json-schema-ref-parser": { 18 | "version": "11.7.2", 19 | "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", 20 | "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", 21 | "license": "MIT", 22 | "dependencies": { 23 | "@jsdevtools/ono": "^7.1.3", 24 | "@types/json-schema": "^7.0.15", 25 | "js-yaml": "^4.1.0" 26 | }, 27 | "engines": { 28 | "node": ">= 16" 29 | }, 30 | "funding": { 31 | "url": "https://github.com/sponsors/philsturgeon" 32 | } 33 | }, 34 | "node_modules/@jsdevtools/ono": { 35 | "version": "7.1.3", 36 | "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", 37 | "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", 38 | "license": "MIT" 39 | }, 40 | "node_modules/@types/json-schema": { 41 | "version": "7.0.15", 42 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 43 | "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 44 | "license": "MIT" 45 | }, 46 | "node_modules/ajv": { 47 | "version": "8.17.1", 48 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", 49 | "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", 50 | "license": "MIT", 51 | "dependencies": { 52 | "fast-deep-equal": "^3.1.3", 53 | "fast-uri": "^3.0.1", 54 | "json-schema-traverse": "^1.0.0", 55 | "require-from-string": "^2.0.2" 56 | }, 57 | "funding": { 58 | "type": "github", 59 | "url": "https://github.com/sponsors/epoberezkin" 60 | } 61 | }, 62 | "node_modules/argparse": { 63 | "version": "2.0.1", 64 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 65 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 66 | "license": "Python-2.0" 67 | }, 68 | "node_modules/fast-deep-equal": { 69 | "version": "3.1.3", 70 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 71 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 72 | "license": "MIT" 73 | }, 74 | "node_modules/fast-uri": { 75 | "version": "3.0.3", 76 | "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", 77 | "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", 78 | "license": "BSD-3-Clause" 79 | }, 80 | "node_modules/js-yaml": { 81 | "version": "4.1.1", 82 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", 83 | "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", 84 | "license": "MIT", 85 | "dependencies": { 86 | "argparse": "^2.0.1" 87 | }, 88 | "bin": { 89 | "js-yaml": "bin/js-yaml.js" 90 | } 91 | }, 92 | "node_modules/json-schema-traverse": { 93 | "version": "1.0.0", 94 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 95 | "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", 96 | "license": "MIT" 97 | }, 98 | "node_modules/require-from-string": { 99 | "version": "2.0.2", 100 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 101 | "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 102 | "license": "MIT", 103 | "engines": { 104 | "node": ">=0.10.0" 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /internal/cli/root_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/spf13/cobra" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestCommandSubcommands(t *testing.T) { 16 | cmd := cli.NewCommand("unit test") 17 | 18 | cases := []struct { 19 | giveName string 20 | }{ 21 | {giveName: "workers"}, 22 | {giveName: "reset"}, 23 | {giveName: "serve"}, 24 | } 25 | 26 | // get all existing subcommands and put into the map 27 | subcommands := make(map[string]*cobra.Command) 28 | for _, sub := range cmd.Commands() { 29 | subcommands[sub.Name()] = sub 30 | } 31 | 32 | for _, tt := range cases { 33 | t.Run(tt.giveName, func(t *testing.T) { 34 | if _, exists := subcommands[tt.giveName]; !exists { 35 | assert.Failf(t, "command not found", "command [%s] was not found", tt.giveName) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestCommandFlags(t *testing.T) { 42 | cmd := cli.NewCommand("unit test") 43 | 44 | cases := []struct { 45 | giveName string 46 | wantShorthand string 47 | wantDefault string 48 | }{ 49 | {giveName: "config", wantShorthand: "c", wantDefault: ".rr.yaml"}, 50 | {giveName: "WorkDir", wantShorthand: "w", wantDefault: ""}, 51 | {giveName: "dotenv", wantShorthand: "", wantDefault: ""}, 52 | {giveName: "debug", wantShorthand: "d", wantDefault: "false"}, 53 | {giveName: "override", wantShorthand: "o", wantDefault: "[]"}, 54 | } 55 | 56 | for _, tt := range cases { 57 | t.Run(tt.giveName, func(t *testing.T) { 58 | flag := cmd.Flag(tt.giveName) 59 | 60 | if flag == nil { 61 | assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName) 62 | 63 | return 64 | } 65 | 66 | assert.Equal(t, tt.wantShorthand, flag.Shorthand) 67 | assert.Equal(t, tt.wantDefault, flag.DefValue) 68 | }) 69 | } 70 | } 71 | 72 | func TestCommandSimpleExecuting(t *testing.T) { 73 | cmd := cli.NewCommand("unit test") 74 | cmd.SetArgs([]string{"-c", "./../../.rr.yaml"}) 75 | 76 | var executed bool 77 | 78 | if cmd.Run == nil { // override "Run" property for test (if it was not set) 79 | cmd.Run = func(_ *cobra.Command, _ []string) { 80 | executed = true 81 | } 82 | } 83 | 84 | assert.NoError(t, cmd.Execute()) 85 | assert.True(t, executed) 86 | } 87 | 88 | func TestCommandNoEnvFileError(t *testing.T) { 89 | cmd := cli.NewCommand("unit test") 90 | cmd.SetArgs([]string{"-c", "./../../.rr.yaml", "--dotenv", "foo/bar"}) 91 | 92 | var executed bool 93 | 94 | if cmd.Run == nil { // override "Run" property for test (if it was not set) 95 | cmd.Run = func(_ *cobra.Command, _ []string) { 96 | executed = true 97 | } 98 | } 99 | 100 | assert.Error(t, cmd.Execute()) 101 | assert.False(t, executed) 102 | } 103 | 104 | func TestCommandNoEnvFileNoError(t *testing.T) { 105 | tmp := os.TempDir() 106 | 107 | cmd := cli.NewCommand("unit test") 108 | cmd.SetArgs([]string{"-c", path.Join(tmp, ".rr.yaml"), "--dotenv", path.Join(tmp, ".env")}) 109 | 110 | var executed bool 111 | 112 | f, err := os.Create(path.Join(tmp, ".env")) 113 | require.NoError(t, err) 114 | f2, err := os.Create(path.Join(tmp, ".rr.yaml")) 115 | require.NoError(t, err) 116 | 117 | defer func() { 118 | _ = f.Close() 119 | _ = f2.Close() 120 | }() 121 | 122 | if cmd.Run == nil { // override "Run" property for test (if it was not set) 123 | cmd.Run = func(_ *cobra.Command, _ []string) { 124 | executed = true 125 | } 126 | } 127 | 128 | assert.NoError(t, cmd.Execute()) 129 | assert.True(t, executed) 130 | 131 | t.Cleanup(func() { 132 | _ = os.RemoveAll(path.Join(tmp, ".env")) 133 | _ = os.RemoveAll(path.Join(tmp, ".rr.yaml")) 134 | }) 135 | } 136 | 137 | func TestCommandWorkingDir(t *testing.T) { 138 | tmp := os.TempDir() 139 | 140 | cmd := cli.NewCommand("serve") 141 | cmd.SetArgs([]string{"-w", tmp}) 142 | 143 | var executed bool 144 | 145 | var wd string 146 | 147 | f2, err := os.Create(path.Join(tmp, ".rr.yaml")) 148 | require.NoError(t, err) 149 | 150 | if cmd.Run == nil { // override "Run" property for test (if it was not set) 151 | cmd.Run = func(_ *cobra.Command, _ []string) { 152 | executed = true 153 | wd, _ = os.Getwd() 154 | } 155 | } 156 | 157 | assert.NoError(t, cmd.Execute()) 158 | assert.True(t, executed) 159 | assert.Equal(t, tmp, wd) 160 | 161 | t.Cleanup(func() { 162 | _ = f2.Close() 163 | _ = os.RemoveAll(path.Join(tmp, ".rr.yaml")) 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /internal/cli/serve/command_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package serve 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/roadrunner-server/endure/v2" 12 | "github.com/roadrunner-server/roadrunner/v2025/container" 13 | "github.com/roadrunner-server/roadrunner/v2025/internal/meta" 14 | "github.com/roadrunner-server/roadrunner/v2025/internal/sdnotify" 15 | 16 | configImpl "github.com/roadrunner-server/config/v5" 17 | "github.com/roadrunner-server/errors" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // log outputs a message to stdout if silent mode is not enabled 22 | func log(msg string, silent bool) { 23 | if !silent { 24 | fmt.Println(msg) 25 | } 26 | } 27 | 28 | func NewCommand(override *[]string, cfgFile *string, silent *bool, experimental *bool) *cobra.Command { //nolint:funlen 29 | return &cobra.Command{ 30 | Use: "serve", 31 | Short: "Start RoadRunner server", 32 | RunE: func(*cobra.Command, []string) error { 33 | const op = errors.Op("handle_serve_command") 34 | // just to be safe 35 | if cfgFile == nil { 36 | return errors.E(op, errors.Str("no configuration file provided")) 37 | } 38 | 39 | // create endure container config 40 | containerCfg, err := container.NewConfig(*cfgFile) 41 | if err != nil { 42 | return errors.E(op, err) 43 | } 44 | 45 | cfg := &configImpl.Plugin{ 46 | Path: *cfgFile, 47 | Timeout: containerCfg.GracePeriod, 48 | Flags: *override, 49 | Version: meta.Version(), 50 | ExperimentalFeatures: *experimental, 51 | } 52 | 53 | endureOptions := []endure.Options{ 54 | endure.GracefulShutdownTimeout(containerCfg.GracePeriod), 55 | } 56 | 57 | if containerCfg.PrintGraph { 58 | endureOptions = append(endureOptions, endure.Visualize()) 59 | } 60 | 61 | // create endure container 62 | ll, err := container.ParseLogLevel(containerCfg.LogLevel) 63 | if err != nil { 64 | log(fmt.Sprintf("[WARN] Failed to parse log level, using default (error): %v", err), *silent) 65 | } 66 | 67 | cont := endure.New(ll, endureOptions...) 68 | 69 | // register plugins 70 | err = cont.RegisterAll(append(container.Plugins(), cfg)...) 71 | if err != nil { 72 | return errors.E(op, err) 73 | } 74 | 75 | // init container and all services 76 | err = cont.Init() 77 | if err != nil { 78 | return errors.E(op, err) 79 | } 80 | 81 | // start serving the graph 82 | errCh, err := cont.Serve() 83 | if err != nil { 84 | return errors.E(op, err) 85 | } 86 | 87 | oss, stop := make(chan os.Signal, 1), make(chan struct{}, 1) 88 | signal.Notify(oss, os.Interrupt, syscall.SIGTERM, syscall.SIGINT, syscall.SIGABRT, syscall.SIGQUIT) 89 | 90 | go func() { 91 | // first catch - stop the container 92 | <-oss 93 | // send signal to stop execution 94 | stop <- struct{}{} 95 | 96 | // notify about stopping 97 | _, _ = sdnotify.SdNotify(sdnotify.Stopping) 98 | 99 | // after the first hit we are waiting for the second catch - exit from the process 100 | <-oss 101 | log("exit forced", *silent) 102 | os.Exit(1) 103 | }() 104 | 105 | log(fmt.Sprintf("[INFO] RoadRunner server started; version: %s, buildtime: %s", meta.Version(), meta.BuildTime()), *silent) 106 | 107 | // at this moment, we're almost sure that the container is running (almost- because we don't know if the plugins won't report an error on the next step) 108 | notified, err := sdnotify.SdNotify(sdnotify.Ready) 109 | if err != nil { 110 | log(fmt.Sprintf("[WARN] sdnotify: %s", err), *silent) 111 | } 112 | 113 | if notified { 114 | log("[INFO] sdnotify: notified", *silent) 115 | stopCh := make(chan struct{}, 1) 116 | if containerCfg.WatchdogSec > 0 { 117 | log(fmt.Sprintf("[INFO] sdnotify: watchdog enabled, timeout: %d seconds", containerCfg.WatchdogSec), *silent) 118 | sdnotify.StartWatchdog(containerCfg.WatchdogSec, stopCh) 119 | } 120 | 121 | // if notified -> notify about stop 122 | defer func() { 123 | stopCh <- struct{}{} 124 | }() 125 | } 126 | 127 | for { 128 | select { 129 | case e := <-errCh: 130 | return fmt.Errorf("error: %w\nplugin: %s", e.Error, e.VertexID) 131 | case <-stop: // stop the container after the first signal 132 | log(fmt.Sprintf("stop signal received, grace timeout is: %0.f seconds", containerCfg.GracePeriod.Seconds()), *silent) 133 | 134 | if err = cont.Stop(); err != nil { 135 | return fmt.Errorf("error: %w", err) 136 | } 137 | 138 | return nil 139 | } 140 | } 141 | }, 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: rr_cli_tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - stable 8 | pull_request: 9 | 10 | jobs: 11 | golangci-lint: 12 | name: Golang-CI (lint) 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v6 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v6 # action page: 20 | with: 21 | go-version: stable 22 | 23 | - name: Run linter 24 | uses: golangci/golangci-lint-action@v9 25 | with: 26 | only-new-issues: false # show only new issues if it's a pull request 27 | args: -v --build-tags=race --timeout=10m 28 | 29 | go-test: 30 | name: Unit tests 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Set up Go 34 | uses: actions/setup-go@v6 35 | with: 36 | go-version: stable 37 | 38 | - name: Check out code 39 | uses: actions/checkout@v6 40 | with: 41 | fetch-depth: 2 # Fixes codecov error 'Issue detecting commit SHA' 42 | 43 | - name: Init Go modules Cache # Docs: 44 | uses: actions/cache@v5 45 | with: 46 | path: ~/go/pkg/mod 47 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 48 | restore-keys: ${{ runner.os }}-go- 49 | 50 | - name: Install Go dependencies 51 | run: go mod download 52 | 53 | - name: Run Unit tests 54 | run: go test -race -covermode=atomic -coverprofile /tmp/coverage.txt ./... 55 | 56 | - name: Upload Coverage report to CodeCov 57 | continue-on-error: true 58 | uses: codecov/codecov-action@v5.5.2 # https://github.com/codecov/codecov-action 59 | with: 60 | files: /tmp/coverage.txt 61 | 62 | build: 63 | name: Build for ${{ matrix.os }} 64 | runs-on: ubuntu-latest 65 | needs: [ go-test ] 66 | strategy: 67 | fail-fast: false 68 | matrix: 69 | os: [ linux, darwin, windows, freebsd ] 70 | steps: 71 | - name: Set up Go 72 | uses: actions/setup-go@v6 # action page: 73 | with: 74 | go-version: stable 75 | 76 | - name: Check out code 77 | uses: actions/checkout@v6 78 | 79 | - name: Init Go modules Cache # Docs: 80 | uses: actions/cache@v5 81 | with: 82 | path: ~/go/pkg/mod 83 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 84 | restore-keys: ${{ runner.os }}-go- 85 | 86 | - name: Install Go dependencies 87 | run: go mod download && go mod verify 88 | 89 | - name: Generate version value 90 | id: values # for PR this value will be `merge@__hash__`, SO: 91 | run: | 92 | echo "version=$(echo ${GITHUB_REF##*/} | sed -e 's/^[vV ]*//')" >> $GITHUB_OUTPUT 93 | echo "timestamp=$(echo $(date +%FT%T%z))" >> $GITHUB_OUTPUT 94 | 95 | - name: Compile binary file 96 | env: 97 | GOOS: ${{ matrix.os }} 98 | GOARCH: amd64 99 | CGO_ENABLED: 0 100 | GOEXPERIMENT: greenteagc 101 | LDFLAGS: -s 102 | -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.version=${{ steps.values.outputs.version }} 103 | -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.buildTime=${{ steps.values.outputs.timestamp }} 104 | run: go build -trimpath -ldflags "$LDFLAGS" -o ./rr ./cmd/rr 105 | 106 | - name: Try to execute 107 | if: matrix.os == 'linux' 108 | run: ./rr -v 109 | 110 | - name: Upload artifact 111 | uses: actions/upload-artifact@v6 112 | with: 113 | name: rr-${{ matrix.os }} 114 | path: ./rr 115 | if-no-files-found: error 116 | retention-days: 10 117 | 118 | docker-image: 119 | name: Build docker image 120 | runs-on: ubuntu-latest 121 | needs: [ go-test ] 122 | steps: 123 | - name: Check out code 124 | uses: actions/checkout@v6 125 | 126 | - name: Build image 127 | run: docker build -t rr:local -f ./Dockerfile . 128 | 129 | - name: Try to execute 130 | run: docker run --rm rr:local -v 131 | 132 | - uses: aquasecurity/trivy-action@0.33.1 # action page: 133 | with: 134 | image-ref: rr:local 135 | format: "table" 136 | severity: HIGH,CRITICAL 137 | exit-code: 1 138 | -------------------------------------------------------------------------------- /.github/workflows/release_dep.yml: -------------------------------------------------------------------------------- 1 | name: release_dep 2 | 3 | on: 4 | release: # Docs: 5 | types: 6 | - released 7 | - prereleased 8 | 9 | jobs: 10 | build: 11 | name: Build for ${{ matrix.os }} (${{ matrix.arch }}, ${{ matrix.compiler }}) 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ linux ] 17 | compiler: [ gcc ] 18 | arch: [ amd64 ] 19 | 20 | steps: 21 | - name: Check out code 22 | uses: actions/checkout@v6 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v6 26 | with: 27 | go-version: stable 28 | 29 | - name: Download dependencies 30 | run: go mod download 31 | 32 | - name: Generate builder values 33 | id: values 34 | run: | 35 | echo "version=$(echo ${GITHUB_REF##*/} | sed -e 's/^[vV ]*//')" >> $GITHUB_OUTPUT 36 | echo "timestamp=$(echo $(date +%FT%T%z))" >> $GITHUB_OUTPUT 37 | echo "binary-name=rr" >> $GITHUB_OUTPUT 38 | echo "sign-cert-name=rr.asc" >> $GITHUB_OUTPUT 39 | 40 | - name: Import GPG key 41 | uses: crazy-max/ghaction-import-gpg@v6 42 | with: 43 | gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} 44 | passphrase: ${{ secrets.GPG_PASS }} 45 | git_user_signingkey: true 46 | git_commit_gpgsign: false 47 | 48 | - name: Compile binary file 49 | env: 50 | GOOS: ${{ matrix.os }} 51 | GOARCH: ${{ matrix.arch }} 52 | CC: ${{ matrix.compiler }} 53 | GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} 54 | GPG_PASS: ${{secrets.GPG_PASS}} 55 | GOEXPERIMENT: greenteagc 56 | CGO_ENABLED: 0 57 | LDFLAGS: >- 58 | -s 59 | -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.version=${{ steps.values.outputs.version }} 60 | -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.buildTime=${{ steps.values.outputs.timestamp }} 61 | run: | 62 | go build -trimpath -ldflags "$LDFLAGS" -o "./${{ steps.values.outputs.binary-name }}" ./cmd/rr 63 | stat "./${{ steps.values.outputs.binary-name }}" 64 | gpg --detach-sign --armor "./${{ steps.values.outputs.binary-name }}" 65 | 66 | - name: Create DEB dirs 67 | run: | 68 | mkdir -p dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/DEBIAN 69 | mkdir -p dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/usr/bin 70 | ls -la dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64 71 | 72 | - name: Create DEB control file 73 | run: touch dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/DEBIAN/control 74 | 75 | - name: Build Debian package 76 | run: | 77 | echo "Package: roadrunner-${{ steps.values.outputs.version }}-linux-amd64.deb" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/DEBIAN/control 78 | echo "Version: ${{ steps.values.outputs.version }}" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/DEBIAN/control 79 | echo "Section: custom" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/DEBIAN/control 80 | echo "Priority: optional" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/DEBIAN/control 81 | echo "Architecture: amd64" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/DEBIAN/control 82 | echo "Maintainer: roadrunner.dev" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/DEBIAN/control 83 | echo "Description: High-performance PHP application server, load-balancer, process manager written in Go and powered with plugins." >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/DEBIAN/control 84 | 85 | cat dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/DEBIAN/control 86 | 87 | cp "./${{ steps.values.outputs.binary-name }}" dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64/usr/bin/ 88 | dpkg --build dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64 89 | 90 | - name: Upload dep to release 91 | uses: svenstaro/upload-release-action@v2 92 | with: 93 | repo_token: ${{ secrets.GITHUB_TOKEN }} 94 | file: dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-amd64.deb 95 | asset_name: roadrunner-${{ steps.values.outputs.version }}-linux-amd64.deb 96 | tag: ${{ github.ref }} 97 | -------------------------------------------------------------------------------- /.github/workflows/release_dep_aarch64.yml: -------------------------------------------------------------------------------- 1 | name: release_dep_arm64 2 | 3 | on: 4 | release: # Docs: 5 | types: 6 | - released 7 | - prereleased 8 | 9 | jobs: 10 | build: 11 | name: Build for ${{ matrix.os }} (${{ matrix.arch }}, ${{ matrix.compiler }}) 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ linux ] 17 | compiler: [ gcc ] 18 | arch: [ arm64 ] 19 | 20 | steps: 21 | - name: Check out code 22 | uses: actions/checkout@v6 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v6 26 | with: 27 | go-version: stable 28 | 29 | - name: Download dependencies 30 | run: go mod download 31 | 32 | - name: Generate builder values 33 | id: values 34 | run: | 35 | echo "version=$(echo ${GITHUB_REF##*/} | sed -e 's/^[vV ]*//')" >> $GITHUB_OUTPUT 36 | echo "timestamp=$(echo $(date +%FT%T%z))" >> $GITHUB_OUTPUT 37 | echo "binary-name=rr" >> $GITHUB_OUTPUT 38 | echo "sign-cert-name=rr.asc" >> $GITHUB_OUTPUT 39 | 40 | - name: Import GPG key 41 | uses: crazy-max/ghaction-import-gpg@v6 42 | with: 43 | gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} 44 | passphrase: ${{ secrets.GPG_PASS }} 45 | git_user_signingkey: true 46 | git_commit_gpgsign: false 47 | 48 | - name: Compile binary file 49 | env: 50 | GOOS: ${{ matrix.os }} 51 | GOARCH: ${{ matrix.arch }} 52 | CC: ${{ matrix.compiler }} 53 | GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} 54 | GPG_PASS: ${{secrets.GPG_PASS}} 55 | CGO_ENABLED: 0 56 | GOEXPERIMENT: greenteagc 57 | LDFLAGS: >- 58 | -s 59 | -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.version=${{ steps.values.outputs.version }} 60 | -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.buildTime=${{ steps.values.outputs.timestamp }} 61 | run: | 62 | go build -trimpath -ldflags "$LDFLAGS" -o "./${{ steps.values.outputs.binary-name }}" ./cmd/rr 63 | stat "./${{ steps.values.outputs.binary-name }}" 64 | gpg --detach-sign --armor "./${{ steps.values.outputs.binary-name }}" 65 | 66 | - name: Create DEB dirs 67 | run: | 68 | mkdir -p dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/DEBIAN 69 | mkdir -p dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/usr/bin 70 | ls -la dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64 71 | 72 | - name: Create DEB control file 73 | run: touch dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/DEBIAN/control 74 | 75 | - name: Build Debian package 76 | run: | 77 | echo "Package: roadrunner-${{ steps.values.outputs.version }}-linux-arm64.deb" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/DEBIAN/control 78 | echo "Version: ${{ steps.values.outputs.version }}" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/DEBIAN/control 79 | echo "Section: custom" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/DEBIAN/control 80 | echo "Priority: optional" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/DEBIAN/control 81 | echo "Architecture: arm64" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/DEBIAN/control 82 | echo "Maintainer: roadrunner.dev" >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/DEBIAN/control 83 | echo "Description: High-performance PHP application server, load-balancer, process manager written in Go and powered with plugins." >> dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/DEBIAN/control 84 | 85 | cat dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/DEBIAN/control 86 | 87 | cp "./${{ steps.values.outputs.binary-name }}" dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64/usr/bin/ 88 | dpkg --build dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64 89 | 90 | - name: Upload dep to release 91 | uses: svenstaro/upload-release-action@v2 92 | with: 93 | repo_token: ${{ secrets.GITHUB_TOKEN }} 94 | file: dist/ubuntu/roadrunner-${{ steps.values.outputs.version }}-linux-arm64.deb 95 | asset_name: roadrunner-${{ steps.values.outputs.version }}-linux-arm64.deb 96 | tag: ${{ github.ref }} 97 | -------------------------------------------------------------------------------- /internal/cli/workers/render.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | import ( 4 | "io" 5 | "sort" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/dustin/go-humanize" 10 | "github.com/fatih/color" 11 | "github.com/olekukonko/tablewriter" 12 | "github.com/olekukonko/tablewriter/tw" 13 | "github.com/roadrunner-server/api/v4/plugins/v4/jobs" 14 | "github.com/roadrunner-server/pool/state/process" 15 | ) 16 | 17 | const ( 18 | Ready string = "READY" 19 | Paused string = "PAUSED/STOPPED" 20 | ) 21 | 22 | // WorkerTable renders table with information about rr server workers. 23 | func WorkerTable(writer io.Writer, workers []*process.State, err error) *tablewriter.Table { 24 | cfg := tablewriter.Config{ 25 | Header: tw.CellConfig{ 26 | Formatting: tw.CellFormatting{ 27 | AutoFormat: tw.On, 28 | }, 29 | }, 30 | MaxWidth: 150, 31 | Row: tw.CellConfig{ 32 | Alignment: tw.CellAlignment{ 33 | Global: tw.AlignLeft, 34 | }, 35 | }, 36 | } 37 | tw := tablewriter.NewTable(writer, tablewriter.WithConfig(cfg)) 38 | tw.Header([]string{"PID", "Status", "Execs", "Memory", "CPU%", "Created"}) 39 | 40 | if err != nil { 41 | _ = tw.Append([]string{ 42 | "0", 43 | err.Error(), 44 | "ERROR", 45 | "ERROR", 46 | "ERROR", 47 | "ERROR", 48 | }) 49 | 50 | return tw 51 | } 52 | 53 | sort.Slice(workers, func(i, j int) bool { 54 | return workers[i].Pid < workers[j].Pid 55 | }) 56 | 57 | for i := range workers { 58 | _ = tw.Append([]string{ 59 | strconv.Itoa(int(workers[i].Pid)), 60 | renderStatus(workers[i].StatusStr), 61 | renderJobs(workers[i].NumExecs), 62 | humanize.Bytes(workers[i].MemoryUsage), 63 | renderCPU(workers[i].CPUPercent), 64 | renderAlive(time.Unix(0, workers[i].Created)), 65 | }) 66 | } 67 | 68 | return tw 69 | } 70 | 71 | // ServiceWorkerTable renders table with information about rr server workers. 72 | func ServiceWorkerTable(writer io.Writer, workers []*process.State) *tablewriter.Table { 73 | sort.Slice(workers, func(i, j int) bool { 74 | return workers[i].Pid < workers[j].Pid 75 | }) 76 | 77 | cfg := tablewriter.Config{ 78 | Header: tw.CellConfig{ 79 | Formatting: tw.CellFormatting{ 80 | AutoFormat: tw.On, 81 | }, 82 | }, 83 | MaxWidth: 150, 84 | Row: tw.CellConfig{ 85 | Alignment: tw.CellAlignment{ 86 | Global: tw.AlignLeft, 87 | }, 88 | }, 89 | } 90 | tw := tablewriter.NewTable(writer, tablewriter.WithConfig(cfg)) 91 | tw.Header([]string{"PID", "Memory", "CPU%", "Command"}) 92 | 93 | for i := range workers { 94 | _ = tw.Append([]string{ 95 | strconv.Itoa(int(workers[i].Pid)), 96 | humanize.Bytes(workers[i].MemoryUsage), 97 | renderCPU(workers[i].CPUPercent), 98 | workers[i].Command, 99 | }) 100 | } 101 | 102 | return tw 103 | } 104 | 105 | // JobsTable renders table with information about rr server jobs. 106 | func JobsTable(writer io.Writer, jobs []*jobs.State, err error) *tablewriter.Table { 107 | cfg := tablewriter.Config{ 108 | Header: tw.CellConfig{ 109 | Formatting: tw.CellFormatting{ 110 | AutoFormat: tw.On, 111 | AutoWrap: int(tw.Off), 112 | }, 113 | }, 114 | MaxWidth: 150, 115 | Row: tw.CellConfig{ 116 | Alignment: tw.CellAlignment{ 117 | Global: tw.AlignLeft, 118 | }, 119 | }, 120 | } 121 | tw := tablewriter.NewTable(writer, tablewriter.WithConfig(cfg)) 122 | tw.Header([]string{"Status", "Pipeline", "Driver", "Queue", "Active", "Delayed", "Reserved"}) 123 | 124 | if err != nil { 125 | _ = tw.Append([]string{ 126 | err.Error(), 127 | "ERROR", 128 | "ERROR", 129 | "ERROR", 130 | "ERROR", 131 | "ERROR", 132 | "ERROR", 133 | }) 134 | 135 | return tw 136 | } 137 | 138 | sort.Slice(jobs, func(i, j int) bool { 139 | return jobs[i].Pipeline < jobs[j].Pipeline 140 | }) 141 | 142 | for i := range jobs { 143 | _ = tw.Append([]string{ 144 | renderReady(jobs[i].Ready), 145 | jobs[i].Pipeline, 146 | jobs[i].Driver, 147 | jobs[i].Queue, 148 | strconv.Itoa(int(jobs[i].Active)), 149 | strconv.Itoa(int(jobs[i].Delayed)), 150 | strconv.Itoa(int(jobs[i].Reserved)), 151 | }) 152 | } 153 | 154 | return tw 155 | } 156 | 157 | func renderReady(ready bool) string { 158 | if ready { 159 | return Ready 160 | } 161 | 162 | return Paused 163 | } 164 | 165 | //go:inline 166 | func renderCPU(cpu float64) string { 167 | return strconv.FormatFloat(cpu, 'f', 2, 64) 168 | } 169 | 170 | func renderStatus(status string) string { 171 | switch status { 172 | case "inactive": 173 | return color.YellowString("inactive") 174 | case "ready": 175 | return color.CyanString("ready") 176 | case "working": 177 | return color.GreenString("working") 178 | case "invalid": 179 | return color.YellowString("invalid") 180 | case "stopped": 181 | return color.RedString("stopped") 182 | case "errored": 183 | return color.RedString("errored") 184 | default: 185 | return status 186 | } 187 | } 188 | 189 | func renderJobs(number uint64) string { 190 | return humanize.Comma(int64(number)) //nolint:gosec 191 | } 192 | 193 | func renderAlive(t time.Time) string { 194 | return humanize.RelTime(t, time.Now(), "ago", "") 195 | } 196 | -------------------------------------------------------------------------------- /download-latest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script can optionally use a GitHub token to increase your request limit (for example, if using this script in a CI). 4 | # To use a GitHub token, pass it through the GITHUB_PAT environment variable. 5 | 6 | # GLOBALS 7 | 8 | # Colors 9 | RED='\033[31m' 10 | GREEN='\033[32m' 11 | DEFAULT='\033[0m' 12 | 13 | # Project name 14 | PNAME='roadrunner' 15 | 16 | # GitHub API address 17 | GITHUB_API='https://api.github.com/repos/roadrunner-server/roadrunner/releases' 18 | # GitHub Release address 19 | GITHUB_REL='https://github.com/roadrunner-server/roadrunner/releases/download' 20 | 21 | # FUNCTIONS 22 | 23 | # Gets the version of the latest stable version of RoadRunner by setting the $latest variable. 24 | # Returns 0 in case of success, 1 otherwise. 25 | get_latest() { 26 | # temp_file is needed because the grep would start before the download is over 27 | temp_file=$(mktemp -q /tmp/$PNAME.XXXXXXXXX) 28 | latest_release="$GITHUB_API/latest" 29 | 30 | if ! temp_file=$(mktemp -q /tmp/$PNAME.XXXXXXXXX); then 31 | echo "$0: Can't create temp file." 32 | fetch_release_failure_usage 33 | exit 1 34 | fi 35 | 36 | if [ -z "$GITHUB_PAT" ]; then 37 | curl -s "$latest_release" >"$temp_file" || return 1 38 | else 39 | curl -H "Authorization: token $GITHUB_PAT" -s "$latest_release" >"$temp_file" || return 1 40 | fi 41 | 42 | latest="$(grep <"$temp_file" '"tag_name":' | cut -d ':' -f2 | tr -d '"' | tr -d ',' | tr -d ' ' | tr -d 'v')" 43 | latestV="$(grep <"$temp_file" '"tag_name":' | cut -d ':' -f2 | tr -d '"' | tr -d ',' | tr -d ' ')" 44 | 45 | rm -f "$temp_file" 46 | return 0 47 | } 48 | 49 | # 0 -> not alpine 50 | # 1 -> alpine 51 | isAlpine() { 52 | # shellcheck disable=SC2143 53 | if [ "$(grep <"/etc/os-release" "NAME=" | grep -ic "Alpine")" ]; then 54 | return 1 55 | fi 56 | 57 | return 0 58 | } 59 | 60 | # Gets the OS by setting the $os variable. 61 | # Returns 0 in case of success, 1 otherwise. 62 | get_os() { 63 | os_name=$(uname -s) 64 | case "$os_name" in 65 | # --- 66 | 'Darwin') 67 | os='darwin' 68 | ;; 69 | 70 | # --- 71 | 'Linux') 72 | os='linux' 73 | if isAlpine; then 74 | os="unknown-musl" 75 | fi 76 | ;; 77 | 78 | # --- 79 | 'MINGW'*) 80 | os='windows' 81 | ;; 82 | 83 | # --- 84 | *) 85 | return 1 86 | ;; 87 | esac 88 | return 0 89 | } 90 | 91 | # Gets the architecture by setting the $arch variable. 92 | # Returns 0 in case of success, 1 otherwise. 93 | get_arch() { 94 | architecture=$(uname -m) 95 | 96 | # case 1 97 | case "$architecture" in 98 | 'x86_64' | 'amd64') 99 | arch='amd64' 100 | ;; 101 | 102 | # case 2 103 | 'arm64' | 'aarch64') 104 | arch='arm64' 105 | ;; 106 | 107 | # all other 108 | *) 109 | return 1 110 | ;; 111 | esac 112 | 113 | return 0 114 | } 115 | 116 | get_compress() { 117 | os_name=$(uname -s) 118 | case "$os_name" in 119 | 'Darwin') 120 | compress='tar.gz' 121 | ;; 122 | 'Linux') 123 | compress='tar.gz' 124 | if isAlpine; then 125 | compress="zip" 126 | fi 127 | ;; 128 | 'MINGW'*) 129 | compress='zip' 130 | ;; 131 | *) 132 | return 1 133 | ;; 134 | esac 135 | return 0 136 | } 137 | 138 | not_available_failure_usage() { 139 | printf "$RED%s\n$DEFAULT" 'ERROR: RoadRunner binary is not available for your OS distribution or your architecture yet.' 140 | echo '' 141 | echo 'However, you can easily compile the binary from the source files.' 142 | echo 'Follow the steps at the page ("Source" tab): TODO' 143 | } 144 | 145 | fetch_release_failure_usage() { 146 | echo '' 147 | printf "$RED%s\n$DEFAULT" 'ERROR: Impossible to get the latest stable version of RoadRunner.' 148 | echo 'Please let us know about this issue: https://github.com/roadrunner-server/roadrunner/issues/new/choose' 149 | echo '' 150 | echo 'In the meantime, you can manually download the appropriate binary from the GitHub release assets here: https://github.com/roadrunner-server/roadrunner/releases/latest' 151 | } 152 | 153 | fill_release_variables() { 154 | # Fill $latest variable. 155 | if ! get_latest; then 156 | fetch_release_failure_usage 157 | exit 1 158 | fi 159 | if [ "$latest" = '' ]; then 160 | fetch_release_failure_usage 161 | exit 1 162 | fi 163 | # Fill $os variable. 164 | if ! get_os; then 165 | not_available_failure_usage 166 | exit 1 167 | fi 168 | # Fill $arch variable. 169 | if ! get_arch; then 170 | not_available_failure_usage 171 | exit 1 172 | fi 173 | 174 | # Fill $compress variable 175 | if ! get_compress; then 176 | not_available_failure_usage 177 | exit 1 178 | fi 179 | } 180 | 181 | download_binary() { 182 | fill_release_variables 183 | echo "Downloading RoadRunner binary $latest for $os, architecture $arch..." 184 | release_file="$PNAME-$latest-$os-$arch.$compress" 185 | 186 | if ! curl --fail -OL "$GITHUB_REL/$latestV/$release_file"; then 187 | fetch_release_failure_usage 188 | exit 1 189 | fi 190 | 191 | printf "$GREEN%s\n$DEFAULT" "RoadRunner $latest archive successfully downloaded as $release_file" 192 | } 193 | 194 | download_binary 195 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | RoadRunner is a high-performance PHP application server and process manager written in Go. It supports running as a service with extensive plugin functionality for HTTP/2/3, gRPC, queues (RabbitMQ, Kafka, SQS, NATS), KV stores, WebSockets, Temporal workflows, and more. 8 | 9 | ## Development Commands 10 | 11 | ### Build 12 | ```bash 13 | make build 14 | # Or manually: 15 | CGO_ENABLED=0 go build -trimpath -ldflags "-s" -o rr cmd/rr/main.go 16 | ``` 17 | 18 | ### Test 19 | ```bash 20 | make test 21 | # Or manually: 22 | go test -v -race ./... 23 | ``` 24 | 25 | ### Debug 26 | ```bash 27 | make debug 28 | # Uses delve to debug with sample config 29 | ``` 30 | 31 | ### Run RoadRunner 32 | ```bash 33 | ./rr serve -c .rr.yaml 34 | ``` 35 | 36 | ### Other Commands 37 | ```bash 38 | ./rr workers # Show worker status 39 | ./rr workers -i # Interactive worker information 40 | ./rr reset # Reset workers 41 | ./rr jobs # Jobs management commands 42 | ./rr stop # Stop RoadRunner server 43 | ``` 44 | 45 | ### Run Single Test 46 | ```bash 47 | go test -v -race -run TestName ./path/to/package 48 | ``` 49 | 50 | ## Architecture 51 | 52 | ### Plugin System 53 | 54 | RoadRunner uses the **Endure** dependency injection container. All plugins are registered in `container/plugins.go:Plugins()`. The plugin architecture follows these principles: 55 | 56 | 1. **Plugin Registration**: Plugins are listed in `container/plugins.go` and automatically wired by Endure 57 | 2. **Plugin Dependencies**: Plugins declare dependencies via struct fields with interface types 58 | 3. **Initialization Order**: Endure resolves the dependency graph and initializes plugins in correct order 59 | 60 | ### Key Components 61 | 62 | - **`cmd/rr/main.go`**: Entry point that delegates to CLI commands 63 | - **`internal/cli/`**: CLI command implementations (serve, workers, reset, jobs, stop) 64 | - **`container/`**: Plugin registration and Endure container configuration 65 | - **Plugin packages**: External packages under `github.com/roadrunner-server/*` (imported in go.mod) 66 | 67 | ### Configuration 68 | 69 | - Primary config: `.rr.yaml` (extensive sample provided) 70 | - Version 3 config format required (`version: '3'`) 71 | - Environment variable substitution supported: `${ENVIRONMENT_VARIABLE_NAME}` 72 | - Sample configs: `.rr-sample-*.yaml` for different use cases (HTTP, gRPC, Temporal, Kafka, etc.) 73 | 74 | ### Core Plugins 75 | 76 | **Server Management:** 77 | - `server`: Worker pool management (NewWorker, NewWorkerPool) 78 | - `rpc`: RPC server for PHP-to-Go communication (default: tcp://127.0.0.1:6001) 79 | - `logger`: Logging infrastructure 80 | - `informer`: Worker status reporting 81 | - `resetter`: Worker reset functionality 82 | 83 | **Protocol Servers:** 84 | - `http`: HTTP/1/2/3 and FastCGI server with middleware support 85 | - `grpc`: gRPC server 86 | - `tcp`: Raw TCP connection handling 87 | 88 | **Jobs/Queue Drivers:** 89 | - `jobs`: Core jobs plugin 90 | - `amqp`, `sqs`, `nats`, `kafka`, `beanstalk`: Queue backends 91 | - `gps`: Google Pub/Sub 92 | 93 | **KV Stores:** 94 | - `kv`: Core KV plugin 95 | - `memory`, `boltdb`, `redis`, `memcached`: Storage backends 96 | 97 | **HTTP Middleware:** 98 | - `static`, `headers`, `gzip`, `prometheus`, `send`, `proxy_ip_parser`, `otel`, `fileserver` 99 | 100 | **Other:** 101 | - `temporal`: Temporal.io workflow engine integration 102 | - `centrifuge`: WebSocket/Broadcast via Centrifugo 103 | - `lock`: Distributed locks 104 | - `metrics`: Prometheus metrics 105 | - `service`: Systemd-like service manager 106 | 107 | ### Worker Communication 108 | 109 | RoadRunner communicates with PHP workers via: 110 | - **Goridge protocol**: Binary protocol over pipes, TCP, or Unix sockets 111 | - **RPC**: For management operations (reset, stats, etc.) 112 | - Workers are PHP processes that implement the RoadRunner worker protocol 113 | 114 | ### Testing 115 | 116 | - Tests use standard Go testing with `-race` flag 117 | - Test files follow `*_test.go` convention 118 | - Sample configs in `.rr-sample-*.yaml` are used for integration tests 119 | - Test directories: `container/test`, `internal/rpc/test` 120 | 121 | ## Important Notes 122 | 123 | - Go version: 1.25+ required (see go.mod) 124 | - Module path: `github.com/roadrunner-server/roadrunner/v2025` 125 | - Some versions are explicitly excluded in go.mod (e.g., go-redis v9.15.0, viper v1.18.x) 126 | - Debug mode available via `--debug` flag (starts debug server on :6061) 127 | - Config overrides supported via `-o dot.notation=value` flag 128 | - Working directory can be set with `-w` flag 129 | - `.env` file support via `--dotenv` flag or `DOTENV_PATH` environment variable 130 | 131 | ## Adding New Plugins 132 | 133 | 1. Import the plugin package in `container/plugins.go` 134 | 2. Add plugin instance to the `Plugins()` slice 135 | 3. Plugin must implement appropriate RoadRunner plugin interfaces 136 | 4. Endure will handle dependency injection and lifecycle management 137 | 138 | ## Configuration Patterns 139 | 140 | - Each plugin has its own configuration section (named after plugin) 141 | - Pools configuration is consistent across plugins (num_workers, max_jobs, timeouts, supervisor) 142 | - TLS configuration follows similar pattern across plugins 143 | - Most plugins support graceful shutdown via timeouts 144 | -------------------------------------------------------------------------------- /internal/cli/serve/command.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package serve 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/roadrunner-server/endure/v2" 12 | "github.com/roadrunner-server/roadrunner/v2025/container" 13 | "github.com/roadrunner-server/roadrunner/v2025/internal/meta" 14 | "github.com/roadrunner-server/roadrunner/v2025/internal/sdnotify" 15 | 16 | configImpl "github.com/roadrunner-server/config/v5" 17 | "github.com/roadrunner-server/errors" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // log outputs a message to stdout if silent mode is not enabled 22 | func log(msg string, silent bool) { 23 | if !silent { 24 | fmt.Println(msg) 25 | } 26 | } 27 | 28 | func NewCommand(override *[]string, cfgFile *string, silent *bool, experimental *bool) *cobra.Command { //nolint:funlen 29 | return &cobra.Command{ 30 | Use: "serve", 31 | Short: "Start RoadRunner server", 32 | RunE: func(*cobra.Command, []string) error { 33 | const op = errors.Op("handle_serve_command") 34 | // just to be safe 35 | if cfgFile == nil { 36 | return errors.E(op, errors.Str("no configuration file provided")) 37 | } 38 | 39 | // create endure container config 40 | containerCfg, err := container.NewConfig(*cfgFile) 41 | if err != nil { 42 | return errors.E(op, err) 43 | } 44 | 45 | cfg := &configImpl.Plugin{ 46 | Path: *cfgFile, 47 | Timeout: containerCfg.GracePeriod, 48 | Flags: *override, 49 | Version: meta.Version(), 50 | ExperimentalFeatures: *experimental, 51 | } 52 | 53 | endureOptions := []endure.Options{ 54 | endure.GracefulShutdownTimeout(containerCfg.GracePeriod), 55 | } 56 | 57 | if containerCfg.PrintGraph { 58 | endureOptions = append(endureOptions, endure.Visualize()) 59 | } 60 | 61 | // create endure container 62 | ll, err := container.ParseLogLevel(containerCfg.LogLevel) 63 | if err != nil { 64 | log(fmt.Sprintf("[WARN] Failed to parse log level, using default (error): %v", err), *silent) 65 | } 66 | 67 | cont := endure.New(ll, endureOptions...) 68 | 69 | // register plugins 70 | err = cont.RegisterAll(append(container.Plugins(), cfg)...) 71 | if err != nil { 72 | return errors.E(op, err) 73 | } 74 | 75 | // init container and all services 76 | err = cont.Init() 77 | if err != nil { 78 | return errors.E(op, err) 79 | } 80 | 81 | // start serving the graph 82 | errCh, err := cont.Serve() 83 | if err != nil { 84 | return errors.E(op, err) 85 | } 86 | 87 | oss, stop := make(chan os.Signal, 1), make(chan struct{}, 1) 88 | signal.Notify(oss, os.Interrupt, syscall.SIGTERM, syscall.SIGINT, syscall.SIGABRT, syscall.SIGQUIT) 89 | 90 | restartCh := make(chan os.Signal, 1) 91 | signal.Notify(restartCh, syscall.SIGUSR2) 92 | 93 | go func() { 94 | // first catch - stop the container 95 | <-oss 96 | // send signal to stop execution 97 | stop <- struct{}{} 98 | 99 | // notify about stopping 100 | _, _ = sdnotify.SdNotify(sdnotify.Stopping) 101 | 102 | // after the first hit we are waiting for the second catch - exit from the process 103 | <-oss 104 | log("exit forced", *silent) 105 | os.Exit(1) 106 | }() 107 | 108 | log(fmt.Sprintf("[INFO] RoadRunner server started; version: %s, buildtime: %s", meta.Version(), meta.BuildTime()), *silent) 109 | 110 | // at this moment, we're almost sure that the container is running (almost- because we don't know if the plugins won't report an error on the next step) 111 | notified, err := sdnotify.SdNotify(sdnotify.Ready) 112 | if err != nil { 113 | log(fmt.Sprintf("[WARN] sdnotify: %s", err), *silent) 114 | } 115 | 116 | if notified { 117 | log("[INFO] sdnotify: notified", *silent) 118 | stopCh := make(chan struct{}, 1) 119 | if containerCfg.WatchdogSec > 0 { 120 | log(fmt.Sprintf("[INFO] sdnotify: watchdog enabled, timeout: %d seconds", containerCfg.WatchdogSec), *silent) 121 | sdnotify.StartWatchdog(containerCfg.WatchdogSec, stopCh) 122 | } 123 | 124 | // if notified -> notify about stop 125 | defer func() { 126 | stopCh <- struct{}{} 127 | }() 128 | } 129 | 130 | for { 131 | select { 132 | case e := <-errCh: 133 | return fmt.Errorf("error: %w\nplugin: %s", e.Error, e.VertexID) 134 | case <-stop: // stop the container after the first signal 135 | log(fmt.Sprintf("stop signal received, grace timeout is: %0.f seconds", containerCfg.GracePeriod.Seconds()), *silent) 136 | 137 | if err = cont.Stop(); err != nil { 138 | return fmt.Errorf("error: %w", err) 139 | } 140 | 141 | return nil 142 | 143 | case <-restartCh: 144 | log("restart signal [SIGUSR2] received", *silent) 145 | executable, err := os.Executable() 146 | if err != nil { 147 | log(fmt.Sprintf("restart failed: %s", err), *silent) 148 | return errors.E("failed to restart") 149 | } 150 | args := os.Args 151 | env := os.Environ() 152 | 153 | if err := cont.Stop(); err != nil { 154 | log(fmt.Sprintf("restart failed: %s", err), *silent) 155 | return errors.E("failed to restart") 156 | } 157 | 158 | err = syscall.Exec(executable, args, env) 159 | if err != nil { 160 | log(fmt.Sprintf("restart failed: %s", err), *silent) 161 | return errors.E("failed to restart") 162 | } 163 | } 164 | } 165 | }, 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /.github/workflows/release_grpc.yml: -------------------------------------------------------------------------------- 1 | name: release_grpc 2 | 3 | on: 4 | release: # Docs: 5 | types: 6 | - prereleased 7 | - released 8 | 9 | jobs: 10 | build: 11 | name: Build for ${{ matrix.os }} (${{ matrix.arch }}, ${{ matrix.compiler }}) 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ windows, darwin ] # linux, darwin, windows 17 | compiler: [ gcc ] # gcc, musl-gcc 18 | archiver: [ zip ] # tar, zip 19 | arch: [ amd64 ] # amd64, 386 20 | include: 21 | - os: linux 22 | compiler: gcc 23 | archiver: tar 24 | arch: amd64 25 | #---------- 26 | - os: linux 27 | compiler: gcc 28 | archiver: tar 29 | arch: arm64 30 | #---------- 31 | - os: freebsd 32 | compiler: gcc 33 | archiver: tar 34 | arch: amd64 35 | #---------- 36 | - os: darwin 37 | compiler: gcc 38 | archiver: tar 39 | arch: arm64 40 | #---------- 41 | - os: '' 42 | compiler: musl-gcc # more info: 43 | archiver: tar 44 | arch: amd64 45 | steps: 46 | - name: Set up Go 47 | uses: actions/setup-go@v6 48 | with: 49 | go-version: stable 50 | 51 | - name: Check out code 52 | uses: actions/checkout@v6 53 | with: 54 | repository: 'roadrunner-server/grpc' 55 | 56 | - name: Install musl 57 | if: matrix.compiler == 'musl-gcc' 58 | run: sudo apt-get install -y musl-tools 59 | 60 | - name: Download dependencies 61 | run: cd protoc_plugins && go mod download 62 | 63 | - name: Generate builder values 64 | id: values 65 | run: | 66 | echo "version=$(echo ${GITHUB_REF##*/} | sed -e 's/^[vV ]*//')" >> $GITHUB_OUTPUT 67 | echo "timestamp=$(echo $(date +%FT%T%z))" >> $GITHUB_OUTPUT 68 | echo "binary-name=$(echo $(echo protoc-gen-php-grpc`[ ${{ matrix.os }} = 'windows' ] && echo '.exe'`))" >> $GITHUB_OUTPUT 69 | if [ ${{ matrix.os }} == "windows" ]; then 70 | echo "sign-cert-name=protoc-gen-php-grpc.exe.asc" >> $GITHUB_OUTPUT 71 | else 72 | echo "sign-cert-name=protoc-gen-php-grpc.asc" >> $GITHUB_OUTPUT 73 | fi 74 | 75 | - name: Import GPG key 76 | uses: crazy-max/ghaction-import-gpg@v6 77 | with: 78 | gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} 79 | passphrase: ${{ secrets.GPG_PASS }} 80 | git_user_signingkey: true 81 | git_commit_gpgsign: false 82 | 83 | - name: Compile binary file 84 | env: 85 | GOOS: ${{ matrix.os }} 86 | GOARCH: ${{ matrix.arch }} 87 | CC: ${{ matrix.compiler }} 88 | CGO_ENABLED: 0 89 | LDFLAGS: >- 90 | -s 91 | run: | 92 | cd protoc_plugins && go build -trimpath -ldflags "$LDFLAGS" -o "../${{ steps.values.outputs.binary-name }}" protoc-gen-php-grpc/main.go 93 | stat "../${{ steps.values.outputs.binary-name }}" 94 | gpg --detach-sign --armor "../${{ steps.values.outputs.binary-name }}" 95 | 96 | - name: Generate distributive directory name 97 | id: dist-dir 98 | run: > 99 | echo "name=$(echo protoc-gen-php-grpc-${{ steps.values.outputs.version }}-$( 100 | [ ${{ matrix.os }} != '' ] && echo '${{ matrix.os }}' || echo 'unknown' 101 | )$( 102 | [ ${{ matrix.compiler }} = 'musl-gcc' ] && echo '-musl' 103 | ))-${{ matrix.arch }}" >> $GITHUB_OUTPUT 104 | 105 | - name: Generate distributive archive name 106 | id: dist-arch 107 | run: > 108 | echo "name=$(echo ${{ steps.dist-dir.outputs.name }}.$( 109 | case ${{ matrix.archiver }} in 110 | zip) echo 'zip';; 111 | tar) echo 'tar.gz';; 112 | *) exit 10; 113 | esac 114 | ))" >> $GITHUB_OUTPUT 115 | 116 | - name: Create distributive 117 | run: | 118 | mkdir ${{ steps.dist-dir.outputs.name }} 119 | mv "./${{ steps.values.outputs.binary-name }}" "./${{ steps.values.outputs.sign-cert-name }}" ./${{ steps.dist-dir.outputs.name }}/ 120 | 121 | - name: Pack distributive using tar 122 | if: matrix.archiver == 'tar' 123 | run: tar -zcf "${{ steps.dist-arch.outputs.name }}" "${{ steps.dist-dir.outputs.name }}" 124 | 125 | - name: Pack distributive using zip 126 | if: matrix.archiver == 'zip' 127 | run: zip -r -q "${{ steps.dist-arch.outputs.name }}" "${{ steps.dist-dir.outputs.name }}" 128 | 129 | - name: Upload artifact 130 | uses: actions/upload-artifact@v6 131 | with: 132 | name: ${{ steps.dist-dir.outputs.name }} 133 | path: ${{ steps.dist-arch.outputs.name }} 134 | if-no-files-found: error 135 | retention-days: 30 136 | 137 | - name: Upload binaries to release 138 | uses: svenstaro/upload-release-action@v2 139 | with: 140 | repo_token: ${{ secrets.GITHUB_TOKEN }} 141 | file: ${{ steps.dist-arch.outputs.name }} 142 | asset_name: ${{ steps.dist-arch.outputs.name }} 143 | tag: ${{ github.ref }} 144 | -------------------------------------------------------------------------------- /internal/cli/root.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | stderr "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "path/filepath" 11 | "runtime" 12 | "strconv" 13 | "syscall" 14 | 15 | "github.com/joho/godotenv" 16 | "github.com/roadrunner-server/errors" 17 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli/jobs" 18 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli/reset" 19 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli/serve" 20 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli/stop" 21 | "github.com/roadrunner-server/roadrunner/v2025/internal/cli/workers" 22 | dbg "github.com/roadrunner-server/roadrunner/v2025/internal/debug" 23 | "github.com/roadrunner-server/roadrunner/v2025/internal/meta" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | const ( 28 | // env var name: path to the .env file 29 | envDotenv string = "DOTENV_PATH" 30 | pidFileName string = ".pid" 31 | ) 32 | 33 | // NewCommand creates root command. 34 | func NewCommand(cmdName string) *cobra.Command { //nolint:funlen,gocognit 35 | // path to the .rr.yaml 36 | cfgFile := toPtr("") 37 | // pidfile path 38 | pidFile := toPtr(false) 39 | // force stop RR 40 | forceStop := toPtr(false) 41 | // override config values 42 | override := &[]string{} 43 | // do not print startup message 44 | silent := toPtr(false) 45 | // enable experimental features 46 | experimental := toPtr(false) 47 | 48 | // working directory 49 | var workDir string 50 | // path to the .env file 51 | var dotenv string 52 | // debug mode 53 | var debug bool 54 | 55 | cmd := &cobra.Command{ 56 | Use: cmdName, 57 | Short: "High-performance PHP application server, process manager written in Golang and powered with ❤️ (by SpiralScout)", 58 | SilenceErrors: true, 59 | SilenceUsage: true, 60 | Version: fmt.Sprintf("%s (build time: %s, %s), OS: %s, arch: %s", meta.Version(), meta.BuildTime(), runtime.Version(), runtime.GOOS, runtime.GOARCH), 61 | PersistentPreRunE: func(*cobra.Command, []string) error { 62 | // cfgFile could be defined by user or default `.rr.yaml` 63 | // this check added just to be safe 64 | if cfgFile == nil || *cfgFile == "" { 65 | return errors.Str("no configuration file provided") 66 | } 67 | 68 | // if user set the wd, change the current wd 69 | if workDir != "" { 70 | if err := os.Chdir(workDir); err != nil { 71 | return err 72 | } 73 | } 74 | 75 | // try to get the absolute path to the configuration 76 | if absPath, err := filepath.Abs(*cfgFile); err == nil { 77 | *cfgFile = absPath // switch a config path to the absolute 78 | 79 | // if workDir is empty - force working absPath related to config file 80 | if workDir == "" { 81 | if err = os.Chdir(filepath.Dir(absPath)); err != nil { 82 | return err 83 | } 84 | } 85 | } 86 | 87 | if v, ok := os.LookupEnv(envDotenv); ok { // read a path to the dotenv file from environment variable 88 | dotenv = v 89 | } 90 | 91 | if dotenv != "" { 92 | err := godotenv.Load(dotenv) 93 | if err != nil { 94 | return err 95 | } 96 | } 97 | 98 | if debug { 99 | srv := dbg.NewServer() 100 | exit := make(chan os.Signal, 1) 101 | stpErr := make(chan error, 1) 102 | signal.Notify(exit, os.Interrupt, syscall.SIGTERM, syscall.SIGINT, syscall.SIGABRT) 103 | 104 | go func() { 105 | errS := srv.Start(":6061") 106 | // errS is always non-nil, this is just double check 107 | if errS != nil && stderr.Is(errS, http.ErrServerClosed) { 108 | return 109 | } 110 | // if we have another type of error - record it 111 | stpErr <- errS 112 | }() 113 | 114 | go func() { 115 | for { 116 | select { 117 | case e := <-stpErr: 118 | // no need to stop the server 119 | fmt.Println(fmt.Errorf("[ERROR] debug server stopped with error: %w", e)) 120 | 121 | return 122 | case <-exit: 123 | _ = srv.Stop(context.Background()) 124 | } 125 | } 126 | }() 127 | } 128 | 129 | // user wanted to write a .pid file 130 | if *pidFile { 131 | f, err := os.Create(pidFileName) 132 | if err != nil { 133 | return err 134 | } 135 | defer func() { 136 | _ = f.Close() 137 | }() 138 | 139 | _, err = f.WriteString(strconv.Itoa(os.Getpid())) 140 | if err != nil { 141 | return err 142 | } 143 | } 144 | 145 | return nil 146 | }, 147 | } 148 | 149 | f := cmd.PersistentFlags() 150 | 151 | f.BoolVarP(experimental, "enable-experimental", "e", false, "enable experimental features") 152 | f.BoolVarP(forceStop, "force", "f", false, "force stop") 153 | f.BoolVarP(pidFile, "pid", "p", false, "create a .pid file") 154 | f.StringVarP(cfgFile, "config", "c", ".rr.yaml", "config file") 155 | f.StringVarP(&workDir, "WorkDir", "w", "", "working directory") 156 | f.StringVarP(&dotenv, "dotenv", "", "", fmt.Sprintf("dotenv file [$%s]", envDotenv)) 157 | f.BoolVarP(&debug, "debug", "d", false, "debug mode") 158 | f.BoolVarP(silent, "silent", "s", false, "do not print startup message") 159 | f.StringArrayVarP(override, "override", "o", nil, "override config value (dot.notation=value)") 160 | 161 | cmd.AddCommand( 162 | workers.NewCommand(cfgFile, override), 163 | reset.NewCommand(cfgFile, override, silent), 164 | serve.NewCommand(override, cfgFile, silent, experimental), 165 | stop.NewCommand(silent, forceStop), 166 | jobs.NewCommand(cfgFile, override, silent), 167 | ) 168 | 169 | return cmd 170 | } 171 | 172 | func toPtr[T any](val T) *T { 173 | return &val 174 | } 175 | -------------------------------------------------------------------------------- /internal/rpc/client.go: -------------------------------------------------------------------------------- 1 | // Package rpc contains wrapper around RPC client ONLY for internal usage. 2 | // Should be in sync with the RPC plugin 3 | package rpc 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "net" 9 | "net/rpc" 10 | "os" 11 | "strings" 12 | 13 | goridgeRpc "github.com/roadrunner-server/goridge/v3/pkg/rpc" 14 | rpcPlugin "github.com/roadrunner-server/rpc/v5" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | const ( 19 | rpcKey string = "rpc.listen" 20 | // default envs 21 | envDefault = ":-" 22 | ) 23 | 24 | // NewClient creates client ONLY for internal usage (communication between our application with RR side). 25 | // Client will be connected to the RPC. 26 | func NewClient(cfg string, flags []string) (*rpc.Client, error) { 27 | v := viper.New() 28 | v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 29 | v.SetConfigFile(cfg) 30 | 31 | err := v.ReadInConfig() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | // override config Flags 37 | if len(flags) > 0 { 38 | for _, f := range flags { 39 | key, val, errP := parseFlag(f) 40 | if errP != nil { 41 | return nil, errP 42 | } 43 | 44 | v.Set(key, val) 45 | } 46 | } 47 | 48 | ver := v.Get(versionKey) 49 | if ver == nil { 50 | return nil, fmt.Errorf("rr configuration file should contain a version e.g: version: 3") 51 | } 52 | 53 | if _, ok := ver.(string); !ok { 54 | return nil, fmt.Errorf("version should be a string: `version: \"3\"`, actual type is: %T", ver) 55 | } 56 | 57 | err = handleInclude(ver.(string), v) 58 | if err != nil { 59 | return nil, fmt.Errorf("failed to handle includes: %w", err) 60 | } 61 | 62 | // rpc.listen might be set by the -o flags or env variable 63 | if !v.IsSet(rpcPlugin.PluginName) { 64 | return nil, errors.New("rpc service not specified in the configuration. Tip: add\n rpc:\n\r listen: rr_rpc_address") 65 | } 66 | 67 | conn, err := Dialer(v.GetString(rpcKey)) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)), nil 73 | } 74 | 75 | // Dialer creates rpc socket Dialer. 76 | func Dialer(addr string) (net.Conn, error) { 77 | dsn := strings.Split(addr, "://") 78 | if len(dsn) != 2 { 79 | return nil, errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") 80 | } 81 | 82 | return net.Dial(dsn[0], dsn[1]) //nolint:noctx 83 | } 84 | 85 | func parseFlag(flag string) (string, string, error) { 86 | if !strings.Contains(flag, "=") { 87 | return "", "", fmt.Errorf("invalid flag `%s`", flag) 88 | } 89 | 90 | parts := strings.SplitN(strings.TrimLeft(flag, " \"'`"), "=", 2) 91 | if len(parts) < 2 { 92 | return "", "", errors.New("usage: -o key=value") 93 | } 94 | 95 | if parts[0] == "" { 96 | return "", "", errors.New("key should not be empty") 97 | } 98 | 99 | if parts[1] == "" { 100 | return "", "", errors.New("value should not be empty") 101 | } 102 | 103 | return strings.Trim(parts[0], " \n\t"), parseValue(strings.Trim(parts[1], " \n\t")), nil 104 | } 105 | 106 | func parseValue(value string) string { 107 | escape := []rune(value)[0] 108 | 109 | if escape == '"' || escape == '\'' || escape == '`' { 110 | value = strings.Trim(value, string(escape)) 111 | value = strings.ReplaceAll(value, fmt.Sprintf("\\%s", string(escape)), string(escape)) 112 | } 113 | 114 | return value 115 | } 116 | 117 | // ExpandVal replaces ${var} or $var in the string based on the mapping function. 118 | // For example, os.ExpandEnv(s) is equivalent to os.Expand(s, os.Getenv). 119 | func ExpandVal(s string, mapping func(string) string) string { 120 | var buf []byte 121 | // ${} is all ASCII, so bytes are fine for this operation. 122 | i := 0 123 | for j := 0; j < len(s); j++ { 124 | if s[j] == '$' && j+1 < len(s) { 125 | if buf == nil { 126 | buf = make([]byte, 0, 2*len(s)) 127 | } 128 | buf = append(buf, s[i:j]...) 129 | name, w := getShellName(s[j+1:]) 130 | if name == "" && w > 0 { //nolint:revive 131 | // Encountered invalid syntax; eat the 132 | // characters. 133 | } else if name == "" { 134 | // Valid syntax, but $ was not followed by a 135 | // name. Leave the dollar character untouched. 136 | buf = append(buf, s[j]) 137 | // parse default syntax 138 | } else if idx := strings.Index(s, envDefault); idx != -1 { 139 | // ${key:=default} or ${key:-val} 140 | substr := strings.Split(name, envDefault) 141 | if len(substr) != 2 { 142 | return "" 143 | } 144 | 145 | key := substr[0] 146 | defaultVal := substr[1] 147 | 148 | res := mapping(key) 149 | if res == "" { 150 | res = defaultVal 151 | } 152 | 153 | buf = append(buf, res...) 154 | } else { 155 | buf = append(buf, mapping(name)...) 156 | } 157 | j += w 158 | i = j + 1 159 | } 160 | } 161 | if buf == nil { 162 | return s 163 | } 164 | return string(buf) + s[i:] 165 | } 166 | 167 | // getShellName returns the name that begins the string and the number of bytes 168 | // consumed to extract it. If the name is enclosed in {}, it's part of a ${} 169 | // expansion, and two more bytes are needed than the length of the name. 170 | func getShellName(s string) (string, int) { 171 | switch { 172 | case s[0] == '{': 173 | if len(s) > 2 && isShellSpecialVar(s[1]) && s[2] == '}' { 174 | return s[1:2], 3 175 | } 176 | // Scan to closing brace 177 | for i := 1; i < len(s); i++ { 178 | if s[i] == '}' { 179 | if i == 1 { 180 | return "", 2 // Bad syntax; eat "${}" 181 | } 182 | return s[1:i], i + 1 183 | } 184 | } 185 | return "", 1 // Bad syntax; eat "${" 186 | case isShellSpecialVar(s[0]): 187 | return s[0:1], 1 188 | } 189 | // Scan alphanumerics. 190 | var i int 191 | for i = 0; i < len(s) && isAlphaNum(s[i]); i++ { //nolint:revive 192 | 193 | } 194 | return s[:i], i 195 | } 196 | 197 | func expandEnvViper(v *viper.Viper) { 198 | for _, key := range v.AllKeys() { 199 | val := v.Get(key) 200 | switch t := val.(type) { 201 | case string: 202 | // for string expand it 203 | v.Set(key, parseEnvDefault(t)) 204 | case []any: 205 | // for slice -> check if it's a slice of strings 206 | strArr := make([]string, 0, len(t)) 207 | for i := range t { 208 | if valStr, ok := t[i].(string); ok { 209 | strArr = append(strArr, parseEnvDefault(valStr)) 210 | continue 211 | } 212 | 213 | v.Set(key, val) 214 | } 215 | 216 | // we should set the whole array 217 | if len(strArr) > 0 { 218 | v.Set(key, strArr) 219 | } 220 | default: 221 | v.Set(key, val) 222 | } 223 | } 224 | } 225 | 226 | // isShellSpecialVar reports whether the character identifies a special 227 | // shell variable such as $*. 228 | func isShellSpecialVar(c uint8) bool { 229 | switch c { 230 | case '*', '#', '$', '@', '!', '?', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 231 | return true 232 | } 233 | return false 234 | } 235 | 236 | // isAlphaNum reports whether the byte is an ASCII letter, number, or underscore. 237 | func isAlphaNum(c uint8) bool { 238 | return c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' 239 | } 240 | 241 | func parseEnvDefault(val string) string { 242 | // tcp://127.0.0.1:${RPC_PORT:-36643} 243 | // for envs like this, part would be tcp://127.0.0.1: 244 | return ExpandVal(val, os.Getenv) 245 | } 246 | -------------------------------------------------------------------------------- /schemas/config/1.0.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "description": "Version 1.0 is deprecated. Please, upgrade RR up to version 2", 4 | "type": "object", 5 | "properties": { 6 | "env": { 7 | "type": "object", 8 | "properties": { 9 | "key": { 10 | "type": "string" 11 | } 12 | } 13 | }, 14 | "rpc": { 15 | "type": "object", 16 | "properties": { 17 | "enable": { 18 | "type": "boolean" 19 | }, 20 | "listen": { 21 | "type": "string" 22 | } 23 | } 24 | }, 25 | "metrics": { 26 | "type": "object", 27 | "properties": { 28 | "address": { 29 | "type": "string" 30 | }, 31 | "collect": { 32 | "type": "object", 33 | "patternProperties": { 34 | "[a-zA-Z0-9-_]": { 35 | "type": "object", 36 | "properties": { 37 | "type": { 38 | "type": "string" 39 | }, 40 | "help": { 41 | "type": "string" 42 | }, 43 | "labels": { 44 | "type": "array", 45 | "items": {} 46 | }, 47 | "buckets": { 48 | "type": "array", 49 | "items": {} 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | }, 57 | "http": { 58 | "type": "object", 59 | "properties": { 60 | "address": { 61 | "type": "string" 62 | }, 63 | "ssl": { 64 | "type": "object", 65 | "properties": { 66 | "port": { 67 | "type": "integer" 68 | }, 69 | "redirect": { 70 | "type": "boolean" 71 | }, 72 | "cert": { 73 | "type": "string" 74 | }, 75 | "key": { 76 | "type": "string" 77 | }, 78 | "rootCa": { 79 | "type": "string" 80 | } 81 | } 82 | }, 83 | "fcgi": { 84 | "type": "object", 85 | "properties": { 86 | "address": { 87 | "type": "string" 88 | } 89 | } 90 | }, 91 | "http2": { 92 | "type": "object", 93 | "properties": { 94 | "enabled": { 95 | "type": "boolean" 96 | }, 97 | "h2c": { 98 | "type": "boolean" 99 | }, 100 | "maxConcurrentStreams": { 101 | "type": "integer" 102 | } 103 | } 104 | }, 105 | "maxRequestSize": { 106 | "type": "integer" 107 | }, 108 | "uploads": { 109 | "type": "object", 110 | "properties": { 111 | "forbid": { 112 | "type": "array", 113 | "items": {} 114 | } 115 | } 116 | }, 117 | "trustedSubnets": { 118 | "type": "array", 119 | "items": {} 120 | }, 121 | "workers": { 122 | "type": "object", 123 | "properties": { 124 | "command": { 125 | "type": "string" 126 | }, 127 | "relay": { 128 | "type": "string" 129 | }, 130 | "user": { 131 | "type": "string" 132 | }, 133 | "pool": { 134 | "type": "object", 135 | "properties": { 136 | "numWorkers": { 137 | "type": "integer" 138 | }, 139 | "maxJobs": { 140 | "type": "integer" 141 | }, 142 | "allocateTimeout": { 143 | "type": "integer" 144 | }, 145 | "destroyTimeout": { 146 | "type": "integer" 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | }, 154 | "headers": { 155 | "type": "object", 156 | "properties": { 157 | "cors": { 158 | "type": "object", 159 | "properties": { 160 | "allowedOrigin": { 161 | "type": "string" 162 | }, 163 | "allowedHeaders": { 164 | "type": "string" 165 | }, 166 | "allowedMethods": { 167 | "type": "string" 168 | }, 169 | "allowCredentials": { 170 | "type": "boolean" 171 | }, 172 | "exposedHeaders": { 173 | "type": "string" 174 | }, 175 | "maxAge": { 176 | "type": "integer" 177 | } 178 | } 179 | }, 180 | "request": { 181 | "type": "object", 182 | "patternProperties": { 183 | "[a-zA-Z0-9-_]": { 184 | "type": "string" 185 | } 186 | } 187 | }, 188 | "response": { 189 | "type": "object", 190 | "patternProperties": { 191 | "[a-zA-Z0-9-_]": { 192 | "type": "string" 193 | } 194 | } 195 | } 196 | } 197 | }, 198 | "limit": { 199 | "type": "object", 200 | "properties": { 201 | "interval": { 202 | "type": "integer" 203 | }, 204 | "services": { 205 | "type": "object", 206 | "properties": { 207 | "http": { 208 | "type": "object", 209 | "properties": { 210 | "maxMemory": { 211 | "type": "integer" 212 | }, 213 | "TTL": { 214 | "type": "integer" 215 | }, 216 | "idleTTL": { 217 | "type": "integer" 218 | }, 219 | "execTTL": { 220 | "type": "integer" 221 | } 222 | } 223 | } 224 | } 225 | } 226 | } 227 | }, 228 | "static": { 229 | "type": "object", 230 | "properties": { 231 | "dir": { 232 | "type": "string" 233 | }, 234 | "forbid": { 235 | "type": "array", 236 | "items": {} 237 | } 238 | } 239 | }, 240 | "health": { 241 | "type": "object", 242 | "properties": { 243 | "address": { 244 | "type": "string" 245 | } 246 | } 247 | }, 248 | "reload": { 249 | "type": "object", 250 | "properties": { 251 | "interval": { 252 | "type": "string" 253 | }, 254 | "patterns": { 255 | "type": "array", 256 | "items": {} 257 | }, 258 | "services": { 259 | "type": "object", 260 | "properties": { 261 | "http": { 262 | "type": "object", 263 | "properties": { 264 | "dirs": { 265 | "type": "array", 266 | "items": {} 267 | }, 268 | "recursive": { 269 | "type": "boolean" 270 | } 271 | } 272 | } 273 | } 274 | } 275 | } 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: # Docs: 5 | types: 6 | - released 7 | - prereleased 8 | 9 | jobs: 10 | build: 11 | name: Build for ${{ matrix.os }} (${{ matrix.arch }}, ${{ matrix.compiler }}) 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ windows, darwin ] # linux, darwin, windows 17 | compiler: [ gcc ] # gcc, musl-gcc 18 | archiver: [ zip ] # tar, zip 19 | arch: [ amd64 ] # amd64, 386 20 | include: 21 | - os: linux 22 | compiler: gcc 23 | archiver: tar 24 | arch: amd64 25 | # ----- 26 | - os: linux 27 | compiler: gcc 28 | archiver: tar 29 | arch: arm64 30 | # ----- 31 | - os: darwin 32 | compiler: gcc 33 | archiver: tar 34 | arch: arm64 35 | # ----- 36 | - os: freebsd 37 | compiler: gcc 38 | archiver: tar 39 | arch: amd64 40 | # ----- 41 | - os: '' 42 | compiler: musl-gcc # more info: 43 | archiver: tar 44 | arch: amd64 45 | steps: 46 | - name: Set up Go 47 | uses: actions/setup-go@v6 48 | with: 49 | go-version: stable 50 | 51 | - name: Check out code 52 | uses: actions/checkout@v6 53 | 54 | - name: Install musl 55 | if: matrix.compiler == 'musl-gcc' 56 | run: sudo apt-get install -y musl-tools 57 | 58 | - name: Download dependencies 59 | run: go mod download # `-x` means "verbose" mode 60 | 61 | - name: Generate builder values 62 | id: values 63 | run: | 64 | echo "version=$(echo ${GITHUB_REF##*/} | sed -e 's/^[vV ]*//')" >> $GITHUB_OUTPUT 65 | echo "timestamp=$(echo $(date +%FT%T%z))" >> $GITHUB_OUTPUT 66 | echo "binary-name=$(echo $(echo rr`[ ${{ matrix.os }} = 'windows' ] && echo '.exe'`))" >> $GITHUB_OUTPUT 67 | if [ ${{ matrix.os }} == "windows" ]; then 68 | echo "sign-cert-name=rr.exe.asc" >> $GITHUB_OUTPUT 69 | else 70 | echo "sign-cert-name=rr.asc" >> $GITHUB_OUTPUT 71 | fi 72 | 73 | - name: Import GPG key 74 | uses: crazy-max/ghaction-import-gpg@v6 75 | with: 76 | gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} 77 | passphrase: ${{ secrets.GPG_PASS }} 78 | git_user_signingkey: true 79 | git_commit_gpgsign: false 80 | 81 | - name: Compile binary file 82 | env: 83 | GOOS: ${{ matrix.os }} 84 | GOARCH: ${{ matrix.arch }} 85 | CC: ${{ matrix.compiler }} 86 | GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} 87 | GPG_PASS: ${{secrets.GPG_PASS}} 88 | CGO_ENABLED: 0 89 | GOEXPERIMENT: greenteagc 90 | LDFLAGS: >- 91 | -s 92 | -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.version=${{ steps.values.outputs.version }} 93 | -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.buildTime=${{ steps.values.outputs.timestamp }} 94 | run: | 95 | go build -trimpath -ldflags "$LDFLAGS" -o "./${{ steps.values.outputs.binary-name }}" ./cmd/rr 96 | stat "./${{ steps.values.outputs.binary-name }}" 97 | gpg --detach-sign --armor "./${{ steps.values.outputs.binary-name }}" 98 | 99 | - name: Generate distributive directory name 100 | id: dist-dir 101 | run: > 102 | echo "name=$(echo roadrunner-${{ steps.values.outputs.version }}-$( 103 | [ ${{ matrix.os }} != '' ] && echo '${{ matrix.os }}' || echo 'unknown' 104 | )$( 105 | [ ${{ matrix.compiler }} = 'musl-gcc' ] && echo '-musl' 106 | ))-${{ matrix.arch }}" >> $GITHUB_OUTPUT 107 | 108 | - name: Generate distributive archive name 109 | id: dist-arch 110 | run: > 111 | echo "name=$(echo ${{ steps.dist-dir.outputs.name }}.$( 112 | case ${{ matrix.archiver }} in 113 | zip) echo 'zip';; 114 | tar) echo 'tar.gz';; 115 | *) exit 10; 116 | esac 117 | ))" >> $GITHUB_OUTPUT 118 | 119 | - name: Create distributive 120 | run: | 121 | mkdir ${{ steps.dist-dir.outputs.name }} 122 | mv "./${{ steps.values.outputs.binary-name }}" "./${{ steps.values.outputs.sign-cert-name }}" ./${{ steps.dist-dir.outputs.name }}/ 123 | cp ./README.md ./CHANGELOG.md ./LICENSE ./${{ steps.dist-dir.outputs.name }} 124 | 125 | - name: Pack distributive using tar 126 | if: matrix.archiver == 'tar' 127 | run: tar -zcf "${{ steps.dist-arch.outputs.name }}" "${{ steps.dist-dir.outputs.name }}" 128 | 129 | - name: Pack distributive using zip 130 | if: matrix.archiver == 'zip' 131 | run: zip -r -q "${{ steps.dist-arch.outputs.name }}" "${{ steps.dist-dir.outputs.name }}" 132 | 133 | - name: Upload artifact 134 | uses: actions/upload-artifact@v6 135 | with: 136 | name: ${{ steps.dist-dir.outputs.name }} 137 | path: ${{ steps.dist-arch.outputs.name }} 138 | if-no-files-found: error 139 | retention-days: 30 140 | 141 | - name: Upload binaries to release 142 | uses: svenstaro/upload-release-action@v2 143 | with: 144 | repo_token: ${{ secrets.GITHUB_TOKEN }} 145 | file: ${{ steps.dist-arch.outputs.name }} 146 | asset_name: ${{ steps.dist-arch.outputs.name }} 147 | tag: ${{ github.ref }} 148 | 149 | docker: 150 | name: Build docker image 151 | runs-on: ubuntu-latest 152 | steps: 153 | - name: Check out code 154 | uses: actions/checkout@v6 155 | 156 | - name: Set up QEMU 157 | uses: docker/setup-qemu-action@v3 # Action page: 158 | 159 | - name: Set up Docker Buildx 160 | uses: docker/setup-buildx-action@v3 # Action page: 161 | 162 | - name: Login to Docker Hub 163 | uses: docker/login-action@v3 164 | with: 165 | username: ${{ secrets.DOCKER_LOGIN }} 166 | password: ${{ secrets.DOCKER_PASSWORD }} 167 | 168 | - name: Login to GitHub Container Registry 169 | uses: docker/login-action@v3 170 | with: 171 | registry: ghcr.io 172 | username: ${{ secrets.GHCR_LOGIN }} 173 | password: ${{ secrets.GHCR_PASSWORD }} 174 | 175 | - name: Generate builder values 176 | id: values 177 | run: | 178 | echo "version_full=$(echo ${GITHUB_REF##*/} | sed -e 's/^[vV ]*//')" >> $GITHUB_OUTPUT 179 | echo "timestamp=$(echo $(date +%FT%T%z))" >> $GITHUB_OUTPUT 180 | 181 | - name: Build image 182 | uses: docker/build-push-action@v6 # Action page: 183 | with: 184 | context: . 185 | file: Dockerfile 186 | push: true 187 | platforms: linux/amd64,linux/arm64 188 | build-args: | 189 | APP_VERSION=${{ steps.values.outputs.version_full}} 190 | BUILD_TIME=${{ steps.values.outputs.timestamp }} 191 | tags: | 192 | spiralscout/roadrunner:${{ steps.values.outputs.version_full}} 193 | spiralscout/roadrunner:latest 194 | spiralscout/roadrunner:2025 195 | spiralscout/roadrunner:2025.1 196 | 197 | ghcr.io/roadrunner-server/roadrunner:${{ steps.values.outputs.version_full}} 198 | ghcr.io/roadrunner-server/roadrunner:latest 199 | ghcr.io/roadrunner-server/roadrunner:2025 200 | ghcr.io/roadrunner-server/roadrunner:2025.1 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | All releases 17 | 18 |

19 | 20 | RoadRunner is an open-source (MIT licensed) high-performance PHP application server, process manager written in Go and powered with plugins ❤️. 21 | It supports running as a service with the ability to extend its functionality on a per-project basis with plugins. 22 | 23 | 24 | # Features 25 | 26 | **RoadRunner** features a range of plugins, including `HTTP(S)/2/3` and `fCGI` servers that are compatible with PSR-7/PSR-17 standards. This is just one of its many capabilities. It serves as an effective alternative to the traditional Nginx+FPM setup, providing improved performance and more flexibility. Its extensive plugin options go far beyond just `HTTP(S)/2/3` and `fCGI` servers, offering a broad range of functionalities: 27 | - Queue drivers: RabbitMQ, Kafka, SQS, Beanstalk, NATS, In-Memory. 28 | - KV drivers: Redis, Memcached, BoltDB, In-Memory. 29 | - OpenTelemetry protocol support (`gRPC`, `http`, `jaeger`). 30 | - [Workflow engine](https://github.com/temporalio/sdk-php) via [Temporal](https://temporal.io). 31 | - `gRPC` server. For increased speed, the `protobuf` extension can be used. 32 | - `HTTP(S)/2/3` and `fCGI` servers features **automatic TLS management**, **103 Early Hints** support and middleware like: Static, Headers, gzip, prometheus (metrics), send (x-sendfile), OTEL, proxy_ip_parser, etc. 33 | - Embedded distribute lock plugin which manages access to shared resources. 34 | - Metrics server (you might easily expose your own). 35 | - WebSockets and Broadcast via [Centrifugo](https://centrifugal.dev) server. 36 | - Systemd-like services manager with auto-restarts, execution time limiter, etc. 37 | - Production-ready. 38 | - And more 😉 39 | 40 | # Join our discord server: [Link](https://discord.gg/TFeEmCs) 41 | 42 |

43 | Official Website | 44 | Documentation | 45 | Forum | 46 | Release schedule | 47 | Ask RoadRunner Guru 48 |

49 | 50 | # Installation 51 | 52 | The easiest way to get the latest RoadRunner version is to use one of the pre-built release binaries, which are available for 53 | OSX, Linux, FreeBSD, and Windows. Instructions for using these binaries are on the GitHub [releases page](https://github.com/roadrunner-server/roadrunner/releases). 54 | 55 | ## Docker: 56 | 57 | To get the roadrunner binary file you can use our docker image: `ghcr.io/roadrunner-server/roadrunner:2025.X.X` (more information about 58 | image and tags can be found [here](https://github.com/roadrunner-server/roadrunner/pkgs/container/roadrunner)). 59 | 60 | ```dockerfile 61 | FROM ghcr.io/roadrunner-server/roadrunner:2025.X.X AS roadrunner 62 | FROM php:8.3-cli 63 | 64 | COPY --from=roadrunner /usr/bin/rr /usr/local/bin/rr 65 | 66 | # USE THE RR 67 | ``` 68 | 69 | Configuration located in the `.rr.yaml` file ([full sample](https://github.com/roadrunner-server/roadrunner/blob/master/.rr.yaml)): 70 | 71 | 72 | ## Installation via Composer 73 | You can also install RoadRunner automatically using the command shipped with the composer package, run: 74 | 75 | ```bash 76 | composer require spiral/roadrunner-cli 77 | ./vendor/bin/rr get-binary 78 | ``` 79 | 80 | Server binary will be available at the root of your project. 81 | 82 | > **Note** 83 | > 84 | > PHP's extensions `php-curl` and `php-zip` are required to download RoadRunner automatically. 85 | > PHP's extensions `php-sockets` need to be installed to run roadrunner. 86 | > Check with `php --modules` your installed extensions. 87 | 88 | 89 | ## Installation option for the Debian-derivatives (Ubuntu, Mint, MX, etc) 90 | 91 | ```bash 92 | wget https://github.com/roadrunner-server/roadrunner/releases/download/v2025.X.X/roadrunner-2025.X.X-linux-amd64.deb 93 | sudo dpkg -i roadrunner-2025.X.X-linux-amd64.deb 94 | ``` 95 | 96 | ## Download the latest release via curl: 97 | ```bash 98 | curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/roadrunner-server/roadrunner/master/download-latest.sh | sh 99 | ``` 100 | 101 | ## MacOS using [Homebrew](https://brew.sh/): 102 | ```bash 103 | brew install roadrunner 104 | ``` 105 | 106 | ## Windows using [Chocolatey](https://community.chocolatey.org/): 107 | ```bash 108 | choco install roadrunner 109 | ``` 110 | 111 | --- 112 | 113 | Configuration can be located in `.rr.yaml` file ([full sample](https://github.com/roadrunner-server/roadrunner/blob/master/.rr.yaml)): 114 | 115 | ```yaml 116 | version: '3' 117 | 118 | rpc: 119 | listen: tcp://127.0.0.1:6001 120 | 121 | server: 122 | command: "php worker.php" 123 | 124 | http: 125 | address: "0.0.0.0:8080" 126 | 127 | logs: 128 | level: error 129 | ``` 130 | 131 | > Read more in [Documentation](https://docs.roadrunner.dev). 132 | 133 | Example Worker: 134 | -------- 135 | 136 | ```php 137 | waitRequest()) { 150 | try { 151 | $rsp = new Psr7\Response(); 152 | $rsp->getBody()->write('Hello world!'); 153 | 154 | $worker->respond($rsp); 155 | } catch (\Throwable $e) { 156 | $worker->getWorker()->error((string)$e); 157 | } 158 | } 159 | ``` 160 | 161 | > [!IMPORTANT] 162 | > If you see the `EOF` error, check that you have installed the PHP packages from [this step](https://github.com/roadrunner-server/roadrunner#installation-via-composer). 163 | > If this does not help, try to execute the command `php worker.php` directly and check the output. 164 | 165 | --- 166 | 167 | ### Available Plugins: [link](https://docs.roadrunner.dev) 168 | 169 | Run: 170 | ---- 171 | To run application server: 172 | 173 | ``` 174 | $ ./rr serve -c .rr.yaml 175 | ``` 176 | 177 | License: 178 | -------- 179 | The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. Maintained 180 | by [Spiral Scout](https://spiralscout.com). 181 | 182 | ## Contributors 183 | 184 | Thanks to all the people who already contributed! 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/roadrunner-server/roadrunner/v2025 2 | 3 | go 1.25.5 4 | 5 | require ( 6 | github.com/buger/goterm v1.0.4 7 | github.com/dustin/go-humanize v1.0.1 8 | github.com/fatih/color v1.18.0 9 | github.com/joho/godotenv v1.5.1 10 | github.com/olekukonko/tablewriter v1.1.2 11 | github.com/roadrunner-server/amqp/v5 v5.2.3 12 | github.com/roadrunner-server/api/v4 v4.23.0 13 | github.com/roadrunner-server/app-logger/v5 v5.1.9 14 | github.com/roadrunner-server/beanstalk/v5 v5.1.9 15 | github.com/roadrunner-server/boltdb/v5 v5.1.9 16 | github.com/roadrunner-server/centrifuge/v5 v5.1.9 17 | github.com/roadrunner-server/config/v5 v5.1.9 18 | github.com/roadrunner-server/endure/v2 v2.6.2 19 | github.com/roadrunner-server/errors v1.4.1 20 | github.com/roadrunner-server/fileserver/v5 v5.1.9 21 | github.com/roadrunner-server/google-pub-sub/v5 v5.1.9 22 | github.com/roadrunner-server/goridge/v3 v3.8.3 23 | github.com/roadrunner-server/grpc/v5 v5.2.3 24 | github.com/roadrunner-server/gzip/v5 v5.1.9 25 | github.com/roadrunner-server/headers/v5 v5.1.9 26 | github.com/roadrunner-server/http/v5 v5.2.8 27 | github.com/roadrunner-server/informer/v5 v5.1.9 28 | github.com/roadrunner-server/jobs/v5 v5.1.9 29 | github.com/roadrunner-server/kafka/v5 v5.2.5 30 | github.com/roadrunner-server/kv/v5 v5.2.9 31 | github.com/roadrunner-server/lock/v5 v5.1.9 32 | github.com/roadrunner-server/logger/v5 v5.1.9 33 | github.com/roadrunner-server/memcached/v5 v5.1.9 34 | github.com/roadrunner-server/memory/v5 v5.2.9 35 | github.com/roadrunner-server/metrics/v5 v5.1.9 36 | github.com/roadrunner-server/nats/v5 v5.1.9 37 | github.com/roadrunner-server/otel/v5 v5.3.1 38 | github.com/roadrunner-server/pool v1.1.3 39 | github.com/roadrunner-server/prometheus/v5 v5.1.8 40 | github.com/roadrunner-server/proxy_ip_parser/v5 v5.1.9 41 | github.com/roadrunner-server/redis/v5 v5.1.10 42 | github.com/roadrunner-server/resetter/v5 v5.1.9 43 | github.com/roadrunner-server/rpc/v5 v5.1.9 44 | github.com/roadrunner-server/send/v5 v5.1.6 45 | github.com/roadrunner-server/server/v5 v5.2.10 46 | github.com/roadrunner-server/service/v5 v5.1.9 47 | github.com/roadrunner-server/sqs/v5 v5.1.9 48 | github.com/roadrunner-server/static/v5 v5.1.7 49 | github.com/roadrunner-server/status/v5 v5.1.9 50 | github.com/roadrunner-server/tcp/v5 v5.1.9 51 | github.com/spf13/cobra v1.10.2 52 | github.com/spf13/viper v1.21.0 53 | github.com/stretchr/testify v1.11.1 54 | github.com/temporalio/roadrunner-temporal/v5 v5.9.0 55 | ) 56 | 57 | exclude ( 58 | github.com/olekukonko/tablewriter v1.1.1 59 | github.com/redis/go-redis/v9 v9.15.0 60 | github.com/redis/go-redis/v9 v9.15.1 61 | github.com/spf13/viper v1.18.0 62 | github.com/spf13/viper v1.18.1 63 | go.temporal.io/api v1.26.1 64 | ) 65 | 66 | require ( 67 | cloud.google.com/go v0.123.0 // indirect 68 | cloud.google.com/go/auth v0.17.0 // indirect 69 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 70 | cloud.google.com/go/compute/metadata v0.9.0 // indirect 71 | cloud.google.com/go/iam v1.5.3 // indirect 72 | cloud.google.com/go/pubsub/v2 v2.3.0 // indirect 73 | github.com/andybalholm/brotli v1.2.0 // indirect 74 | github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect 75 | github.com/aws/aws-sdk-go-v2/config v1.32.5 // indirect 76 | github.com/aws/aws-sdk-go-v2/credentials v1.19.5 // indirect 77 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect 78 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect 79 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect 80 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect 81 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect 82 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect 83 | github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect 84 | github.com/aws/aws-sdk-go-v2/service/sqs v1.42.20 // indirect 85 | github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect 86 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect 87 | github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect 88 | github.com/aws/smithy-go v1.24.0 // indirect 89 | github.com/beanstalkd/go-beanstalk v0.2.0 // indirect 90 | github.com/beorn7/perks v1.0.1 // indirect 91 | github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect 92 | github.com/cactus/go-statsd-client/v5 v5.1.0 // indirect 93 | github.com/caddyserver/certmagic v0.25.0 // indirect 94 | github.com/caddyserver/zerossl v0.1.3 // indirect 95 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 96 | github.com/cenkalti/backoff/v5 v5.0.3 // indirect 97 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 98 | github.com/clipperhouse/displaywidth v0.6.2 // indirect 99 | github.com/clipperhouse/stringish v0.1.1 // indirect 100 | github.com/clipperhouse/uax29/v2 v2.3.0 // indirect 101 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 102 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 103 | github.com/emicklei/proto v1.14.2 // indirect 104 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect 105 | github.com/felixge/httpsnoop v1.0.4 // indirect 106 | github.com/fsnotify/fsnotify v1.9.0 // indirect 107 | github.com/go-logr/logr v1.4.3 // indirect 108 | github.com/go-logr/stdr v1.2.2 // indirect 109 | github.com/go-ole/go-ole v1.3.0 // indirect 110 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 111 | github.com/goccy/go-json v0.10.5 // indirect 112 | github.com/gofiber/fiber/v2 v2.52.10 // indirect 113 | github.com/gogo/protobuf v1.3.2 // indirect 114 | github.com/golang/mock v1.7.0-rc.1 // indirect 115 | github.com/google/s2a-go v0.1.9 // indirect 116 | github.com/google/uuid v1.6.0 // indirect 117 | github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect 118 | github.com/googleapis/gax-go/v2 v2.15.0 // indirect 119 | github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect 120 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect 121 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 122 | github.com/klauspost/compress v1.18.2 // indirect 123 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 124 | github.com/libdns/libdns v1.1.1 // indirect 125 | github.com/mattn/go-colorable v0.1.14 // indirect 126 | github.com/mattn/go-isatty v0.0.20 // indirect 127 | github.com/mattn/go-runewidth v0.0.19 // indirect 128 | github.com/mholt/acmez v1.2.0 // indirect 129 | github.com/mholt/acmez/v3 v3.1.4 // indirect 130 | github.com/miekg/dns v1.1.69 // indirect 131 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 132 | github.com/nats-io/nats.go v1.47.0 // indirect 133 | github.com/nats-io/nkeys v0.4.12 // indirect 134 | github.com/nats-io/nuid v1.0.1 // indirect 135 | github.com/nexus-rpc/sdk-go v0.5.1 // indirect 136 | github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect 137 | github.com/olekukonko/errors v1.1.0 // indirect 138 | github.com/olekukonko/ll v0.1.3 // indirect 139 | github.com/openzipkin/zipkin-go v0.4.3 // indirect 140 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 141 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 142 | github.com/pkg/errors v0.9.1 // indirect 143 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 144 | github.com/prometheus/client_golang v1.23.2 // indirect 145 | github.com/prometheus/client_model v0.6.2 // indirect 146 | github.com/prometheus/common v0.67.4 // indirect 147 | github.com/prometheus/procfs v0.19.2 // indirect 148 | github.com/quic-go/qpack v0.6.0 // indirect 149 | github.com/quic-go/quic-go v0.57.1 // indirect 150 | github.com/rabbitmq/amqp091-go v1.10.0 // indirect 151 | github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2 // indirect 152 | github.com/redis/go-redis/extra/redisotel/v9 v9.17.2 // indirect 153 | github.com/redis/go-redis/extra/redisprometheus/v9 v9.17.2 // indirect 154 | github.com/redis/go-redis/v9 v9.17.2 // indirect 155 | github.com/roadrunner-server/context v1.1.0 // indirect 156 | github.com/roadrunner-server/events v1.0.1 // indirect 157 | github.com/roadrunner-server/priority_queue v1.0.6 // indirect 158 | github.com/roadrunner-server/tcplisten v1.5.2 // indirect 159 | github.com/robfig/cron v1.2.0 // indirect 160 | github.com/rs/cors v1.11.1 // indirect 161 | github.com/sagikazarmark/locafero v0.12.0 // indirect 162 | github.com/shirou/gopsutil v3.21.11+incompatible // indirect 163 | github.com/spf13/afero v1.15.0 // indirect 164 | github.com/spf13/cast v1.10.0 // indirect 165 | github.com/spf13/pflag v1.0.10 // indirect 166 | github.com/stretchr/objx v0.5.3 // indirect 167 | github.com/subosito/gotenv v1.6.0 // indirect 168 | github.com/tklauser/go-sysconf v0.3.16 // indirect 169 | github.com/tklauser/numcpus v0.11.0 // indirect 170 | github.com/twmb/franz-go v1.20.5 // indirect 171 | github.com/twmb/franz-go/pkg/kmsg v1.12.0 // indirect 172 | github.com/twmb/murmur3 v1.1.8 // indirect 173 | github.com/uber-go/tally/v4 v4.1.17 // indirect 174 | github.com/valyala/bytebufferpool v1.0.0 // indirect 175 | github.com/valyala/fasthttp v1.68.0 // indirect 176 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 177 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 178 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 179 | github.com/zeebo/assert v1.3.1 // indirect 180 | github.com/zeebo/blake3 v0.2.4 // indirect 181 | go.etcd.io/bbolt v1.4.3 // indirect 182 | go.opencensus.io v0.24.0 // indirect 183 | go.opentelemetry.io/auto/sdk v1.2.1 // indirect 184 | go.opentelemetry.io/contrib/instrumentation/github.com/bradfitz/gomemcache/memcache/otelmemcache v0.43.0 // indirect 185 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect 186 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect 187 | go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect 188 | go.opentelemetry.io/otel v1.39.0 // indirect 189 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect 190 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect 191 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect 192 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect 193 | go.opentelemetry.io/otel/exporters/zipkin v1.39.0 // indirect 194 | go.opentelemetry.io/otel/metric v1.39.0 // indirect 195 | go.opentelemetry.io/otel/sdk v1.39.0 // indirect 196 | go.opentelemetry.io/otel/trace v1.39.0 // indirect 197 | go.opentelemetry.io/proto/otlp v1.9.0 // indirect 198 | go.temporal.io/api v1.59.0 // indirect 199 | go.temporal.io/sdk v1.38.0 // indirect 200 | go.temporal.io/sdk/contrib/opentelemetry v0.6.0 // indirect 201 | go.temporal.io/sdk/contrib/tally v0.2.0 // indirect 202 | go.temporal.io/server v1.29.1 // indirect 203 | go.uber.org/atomic v1.11.0 // indirect 204 | go.uber.org/multierr v1.11.0 // indirect 205 | go.uber.org/zap v1.27.1 // indirect 206 | go.uber.org/zap/exp v0.3.0 // indirect 207 | go.yaml.in/yaml/v2 v2.4.3 // indirect 208 | go.yaml.in/yaml/v3 v3.0.4 // indirect 209 | golang.org/x/crypto v0.46.0 // indirect 210 | golang.org/x/mod v0.31.0 // indirect 211 | golang.org/x/net v0.48.0 // indirect 212 | golang.org/x/oauth2 v0.34.0 // indirect 213 | golang.org/x/sync v0.19.0 // indirect 214 | golang.org/x/sys v0.39.0 // indirect 215 | golang.org/x/text v0.32.0 // indirect 216 | golang.org/x/time v0.14.0 // indirect 217 | golang.org/x/tools v0.40.0 // indirect 218 | google.golang.org/api v0.257.0 // indirect 219 | google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2 // indirect 220 | google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect 221 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect 222 | google.golang.org/grpc v1.77.0 // indirect 223 | google.golang.org/protobuf v1.36.11 // indirect 224 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 225 | gopkg.in/yaml.v3 v3.0.1 // indirect 226 | ) 227 | --------------------------------------------------------------------------------