-
Notifications
You must be signed in to change notification settings - Fork 22
Add project executor framework for workflow load testing #301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| name: Project Tests | ||
|
|
||
| permissions: | ||
| contents: read | ||
| actions: write | ||
| checks: write | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: | ||
| - main | ||
|
|
||
| jobs: | ||
| test-go-projecttests: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v4 | ||
| - uses: actions/setup-go@v4 | ||
| with: | ||
| go-version-file: "go.mod" | ||
| - name: Run Go project tests | ||
| run: | | ||
| go test -v -race -timeout 10m ./projecttests/... 2>&1 | \ | ||
| go run github.com/jstemmer/go-junit-report/v2@latest \ | ||
| -set-exit-code -iocopy -out junit-projecttests-go.xml | ||
| - name: Publish Test Results | ||
| uses: mikepenz/action-junit-report@v5.6.2 | ||
| if: failure() | ||
| with: | ||
| report_paths: "junit-projecttests-go.xml" | ||
| check_name: "Go Project Test Failures" | ||
| detailed_summary: true | ||
| check_annotations: false | ||
| annotate_only: true | ||
| skip_annotations: true | ||
|
|
||
| build-go-projecttest-image: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v4 | ||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
| - name: Build project test base image | ||
| run: | | ||
| docker build \ | ||
| -f projecttests/dockerfiles/projecttest-go-base.Dockerfile \ | ||
| -t omes-projecttest-go-base:ci \ | ||
| . | ||
| - name: Build helloworld project overlay | ||
| run: | | ||
| docker build \ | ||
| -f projecttests/dockerfiles/projecttest-go.Dockerfile \ | ||
| --build-arg BASE_IMAGE=omes-projecttest-go-base:ci \ | ||
| --build-arg TEST_PROJECT=projecttests/go/tests/helloworld \ | ||
| -t omes-projecttest-helloworld:ci \ | ||
| . | ||
| - name: Smoke test overlay image | ||
| run: | | ||
| docker run --rm omes-projecttest-helloworld:ci --help | ||
|
Comment on lines
+60
to
+62
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I'd call that a smoke test? |
||
|
|
||
| check-project-protos: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v4 | ||
| - name: Install buf | ||
| uses: bufbuild/buf-action@v1 | ||
| with: | ||
| setup_only: true | ||
| - name: Generate project protos | ||
| run: | | ||
| cd projecttests/proto && buf generate | ||
| - name: Lint project protos | ||
| run: | | ||
| cd projecttests/proto && buf lint | ||
| - name: Check for uncommitted changes | ||
| run: | | ||
| git diff --exit-code projecttests/go/harness/api/ || \ | ||
| (echo "Project proto generated code is out of date. Run 'cd projecttests/proto && buf generate'" && exit 1) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "syscall" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/temporalio/omes/cmd/clioptions" | ||
| "github.com/temporalio/omes/internal/programbuild" | ||
| "go.uber.org/zap" | ||
| ) | ||
|
|
||
| func execCmd() *cobra.Command { | ||
| var r execRunner | ||
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "exec --language <lang> -- <program-args>", | ||
| Short: "Build a project and run it as a subprocess", | ||
| Long: `Build a test project and run the resulting binary as a subprocess. | ||
| Arguments after "--" are passed directly to the subprocess. | ||
|
|
||
| Signal forwarding and graceful shutdown are handled automatically. | ||
|
|
||
| Examples: | ||
| # Start a worker on a specific task queue | ||
| omes exec --language go --project-dir ./projecttests/go/tests/helloworld -- worker --task-queue my-queue | ||
|
|
||
| # Start a project gRPC server on port 8080 | ||
| omes exec --language go --project-dir ./projecttests/go/tests/helloworld -- project-server --port 8080 | ||
|
|
||
| # Start a worker with process metrics sidecar for CPU/memory monitoring | ||
| omes exec --language go --project-dir ./projecttests/go/tests/helloworld --process-monitor-addr :9091 -- worker --task-queue my-queue`, | ||
| PreRunE: func(cmd *cobra.Command, args []string) error { | ||
| r.preRun() | ||
| return r.validate() | ||
| }, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx, cancel := withCancelOnInterrupt(cmd.Context()) | ||
| defer cancel() | ||
| return r.run(ctx, args) | ||
| }, | ||
| } | ||
|
|
||
| r.sdkOpts.AddCLIFlags(cmd.Flags()) | ||
| cmd.Flags().Lookup("language").Usage = "Language to use for workflow tests (go only)" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It won't be Go only except currently though? Also, seemingly the language flag is redundant, since the projecttests path includes the language |
||
| r.programOpts.AddFlags(cmd.Flags()) | ||
| cmd.Flags().AddFlagSet(r.loggingOpts.FlagSet()) | ||
| cmd.Flags().StringVar(&r.processMonitorAddr, "process-monitor-addr", "", "Address for process metrics sidecar (e.g. :9091)") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be just a port number? Seems like it would not make sense to ever bind to anything other than 0.0.0.0? |
||
|
|
||
| cmd.MarkFlagRequired("language") | ||
| cmd.MarkFlagRequired("project-dir") | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| type execRunner struct { | ||
| sdkOpts clioptions.SdkOptions | ||
| programOpts clioptions.ProgramOptions | ||
| loggingOpts clioptions.LoggingOptions | ||
| processMonitorAddr string | ||
|
|
||
| logger *zap.SugaredLogger | ||
| } | ||
|
|
||
| func (r *execRunner) preRun() { | ||
| r.logger = r.loggingOpts.MustCreateLogger() | ||
| } | ||
|
|
||
| func (r *execRunner) validate() error { | ||
| if r.sdkOpts.Language != clioptions.LangGo { | ||
| return fmt.Errorf("--language must be go for workflow tests (got %q)", r.sdkOpts.Language) | ||
| } | ||
| if r.programOpts.ProgramDir == "" { | ||
| return fmt.Errorf("--project-dir is required") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (r *execRunner) run(ctx context.Context, args []string) error { | ||
| builder := &programbuild.ProgramBuilder{ | ||
| Language: r.sdkOpts.Language.String(), | ||
| ProjectDir: r.programOpts.ProgramDir, | ||
| BuildDir: r.programOpts.BuildDir, | ||
| Logger: r.logger, | ||
| } | ||
|
|
||
| prog, err := builder.BuildProgram(ctx, r.sdkOpts.Version) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to build program: %w", err) | ||
| } | ||
|
|
||
| cmd, err := programbuild.StartProgramProcess(ctx, prog, args) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to start program: %w", err) | ||
| } | ||
|
|
||
| r.logger.Infof("Started subprocess (PID %d): %v", cmd.Process.Pid, args) | ||
|
|
||
| // Start process metrics sidecar (if requested) | ||
| if r.processMonitorAddr != "" { | ||
| sidecar := clioptions.StartProcessMetricsSidecar( | ||
| r.logger, | ||
| r.processMonitorAddr, | ||
| cmd.Process.Pid, | ||
| r.sdkOpts.Version, | ||
| "", | ||
| r.sdkOpts.Language.String(), | ||
| ) | ||
| defer func() { | ||
| if err := sidecar.Shutdown(context.Background()); err != nil { | ||
| r.logger.Warnf("Failed to stop process metrics sidecar: %v", err) | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| err = cmd.Wait() | ||
| if err != nil { | ||
| if cmd.ProcessState != nil { | ||
| if status, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok { | ||
| return fmt.Errorf("process exited with code %d", status.ExitStatus()) | ||
| } | ||
| return fmt.Errorf("process exited with code 1") | ||
| } | ||
| return fmt.Errorf("failed waiting for process: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably just be two separate steps