Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,17 @@ jobs:
strategy:
fail-fast: false
matrix:
sdk: [python]
include:
- sdk: python
project: helloworld
dockerfile: dockerfiles/python.Dockerfile
sdk_version_env: PYTHON_SDK_VERSION
sdk_version_prefix: v
- sdk: typescript
project: helloworld
dockerfile: dockerfiles/typescript.Dockerfile
sdk_version_env: TYPESCRIPT_SDK_VERSION
sdk_version_prefix: ""
steps:
- name: Checkout repo
uses: actions/checkout@v4
Expand All @@ -173,7 +179,8 @@ jobs:
printenv >> $GITHUB_ENV
- name: Set ${{ matrix.sdk }} project image metadata
run: |
echo "PROJECT_IMAGE_SDK_VERSION=v${PYTHON_SDK_VERSION}" >> "$GITHUB_ENV"
sdk_version_env=${{ matrix.sdk_version_env }}
echo "PROJECT_IMAGE_SDK_VERSION=${{ matrix.sdk_version_prefix }}${!sdk_version_env}" >> "$GITHUB_ENV"
echo "PROJECT_IMAGE_TAG=omes-project-${{ matrix.sdk }}-${{ matrix.project }}" >> "$GITHUB_ENV"
echo "PROJECT_WORKER_CONTAINER=omes-project-${{ matrix.sdk }}-${{ matrix.project }}-worker" >> "$GITHUB_ENV"
echo "PROJECT_RUN_ID=project-${{ github.run_id }}-${{ matrix.sdk }}-${{ matrix.project }}" >> "$GITHUB_ENV"
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ As such, it's a **good fit** if you:
- are more familiar with Temporal-native code than Omes's framework

and are not restricted by the **current limitations**:
- Python is the only implemented project language right now
- Python and TypeScript are the implemented project languages right now
- the load pattern is limited to a steady-rate executor (i.e. "run 'x' times or run for 'y' duration),
more nuanced load patterns will need to create their own scenario + executor (the existing method)

Expand Down Expand Up @@ -310,6 +310,16 @@ docker stop omes-python-project-worker
docker network rm omes-project-net
```

The TypeScript project image uses the same project arguments with `dockerfiles/typescript.Dockerfile`:

```sh
docker build \
-f dockerfiles/typescript.Dockerfile \
--build-arg PROJECT_NAME=helloworld \
--build-arg SDK_VERSION=1.15.0 \
-t omes-typescript-project-helloworld .
```

This docker workflow it is not yet wired into `go run ./cmd/dev build-worker-image`.

### ThroughputStress
Expand Down
27 changes: 19 additions & 8 deletions dockerfiles/typescript.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ COPY go.mod go.sum ./
RUN CGO_ENABLED=0 /usr/local/go/bin/go build -o temporal-omes ./cmd

ARG SDK_VERSION
ARG PROJECT_NAME=""

# Optional SDK dir to copy, defaults to unimportant file
ARG SDK_DIR=.gitignore
Expand All @@ -52,17 +53,27 @@ COPY workers/typescript ./workers/typescript
RUN npm install -g pnpm

# prepare-worker builds the TypeScript workspace itself: it installs npm deps,
# runs the root build, and generates the prepared sdkbuild package.
RUN CGO_ENABLED=0 ./temporal-omes prepare-worker --language ts --dir-name prepared --version "$SDK_VERSION"
# runs the relevant build, and generates the prepared sdkbuild package.
RUN if [ -n "$PROJECT_NAME" ]; then \
CGO_ENABLED=0 ./temporal-omes prepare-worker --language ts --project-name "$PROJECT_NAME" --dir-name "project-build-runner-$PROJECT_NAME" --version "$SDK_VERSION" ; \
else \
CGO_ENABLED=0 ./temporal-omes prepare-worker --language ts --dir-name prepared --version "$SDK_VERSION" ; \
fi

# Copy the CLI and prepared feature to a "run" container.
# hadolint ignore=DL3006
FROM --platform=linux/$TARGETARCH gcr.io/distroless/nodejs20-debian11
FROM --platform=linux/$TARGETARCH node:20-bullseye-slim

ARG PROJECT_NAME=""

COPY --from=build /app/temporal-omes /app/temporal-omes
COPY --from=build /app/workers/typescript /app/workers/typescript
COPY dockerfiles/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh

ENV OMES_WORKER_LANGUAGE=typescript
ENV OMES_PREPARED_DIR=prepared
ENV OMES_PROJECT_NAME=$PROJECT_NAME
ENV OMES_PROJECT_PREPARED_DIR=project-build-runner-${PROJECT_NAME}
ENV OMES_PROJECT_PREBUILT_DIR=/app/workers/typescript/projects/tests/${PROJECT_NAME}/project-build-runner-${PROJECT_NAME}

# Node is installed here 👇 in distroless
ENV PATH="/nodejs/bin:$PATH"
# Use entrypoint instead of command to "bake" the default command options
ENTRYPOINT ["/app/temporal-omes", "run-worker", "--language", "typescript", "--dir-name", "prepared"]
ENTRYPOINT ["/app/entrypoint.sh"]
4 changes: 3 additions & 1 deletion scenarios/project/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func buildProject(ctx context.Context, repoRoot string, p projectScenarioOptions

baseDir := workers.BaseDir(repoRoot, p.sdkOpts.Language)
switch p.sdkOpts.Language {
case clioptions.LangPython:
case clioptions.LangPython, clioptions.LangTypeScript:
return b.Build(ctx, baseDir)
default:
return nil, fmt.Errorf("unsupported language for project builds: %s", b.SdkOptions.Language)
Expand All @@ -33,6 +33,8 @@ func loadPrebuilt(dir string, lang clioptions.Language) (sdkbuild.Program, error
switch lang {
case clioptions.LangPython:
return sdkbuild.PythonProgramFromDir(dir)
case clioptions.LangTypeScript:
return sdkbuild.TypeScriptProgramFromDir(dir)
default:
return nil, fmt.Errorf("prebuilt projects not supported for language: %s", lang)
}
Expand Down
9 changes: 6 additions & 3 deletions scenarios/project/handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ type ProjectHandle struct {
client api.ProjectServiceClient
}

func newProjectHandle(ctx context.Context, port int, req *api.InitRequest) (ProjectHandle, error) {
func newProjectHandle(ctx context.Context, port int, req *api.InitRequest, readyTimeout time.Duration) (ProjectHandle, error) {
address := fmt.Sprintf("%s:%d", defaultClientHost, port)
c := ProjectHandle{address: address, taskQueue: req.GetTaskQueue()}

deadline := time.Now().Add(defaultClientReadyTimeout)
if readyTimeout == 0 {
readyTimeout = defaultClientReadyTimeout
}
deadline := time.Now().Add(readyTimeout)
var err error
for time.Now().Before(deadline) {
conn, dialErr := net.Dial("tcp", c.address)
Expand All @@ -47,7 +50,7 @@ func newProjectHandle(ctx context.Context, port int, req *api.InitRequest) (Proj
}
}
if err != nil {
return ProjectHandle{}, fmt.Errorf("project server not ready after %v: %w", defaultClientReadyTimeout, err)
return ProjectHandle{}, fmt.Errorf("project server not ready after %v: %w", readyTimeout, err)
}

conn, err := grpc.NewClient(
Expand Down
18 changes: 12 additions & 6 deletions scenarios/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ type projectScenarioOptions struct {
configJSON []byte
}

type projectScenarioExecutor struct{}
type projectScenarioExecutor struct {
clientReadyTimeout time.Duration
}

func (e *projectScenarioExecutor) Run(ctx context.Context, info loadgen.ScenarioInfo) error {
opts, err := e.validate(info)
Expand Down Expand Up @@ -91,7 +93,7 @@ func (e *projectScenarioExecutor) Run(ctx context.Context, info loadgen.Scenario
DisableHostVerification: co.DisableHostVerification,
},
ConfigJson: opts.configJSON,
})
}, e.clientReadyTimeout)
if err != nil {
return fmt.Errorf("failed to init project: %w", err)
}
Expand All @@ -111,8 +113,8 @@ func (e *projectScenarioExecutor) validate(info loadgen.ScenarioInfo) (projectSc
if err := opts.sdkOpts.Language.Set(lang); err != nil {
return opts, fmt.Errorf("unrecognized language: %s", lang)
}
if opts.sdkOpts.Language != clioptions.LangPython {
return opts, fmt.Errorf("project scenario is currently limited to Python, got %s", lang)
if opts.sdkOpts.Language != clioptions.LangPython && opts.sdkOpts.Language != clioptions.LangTypeScript {
return opts, fmt.Errorf("project scenario is currently limited to Python and TypeScript, got %s", lang)
}

projectName := info.ScenarioOptions["project-name"]
Expand Down Expand Up @@ -161,9 +163,13 @@ func findAvailablePort() (int, error) {

func startProjectProcess(ctx context.Context, prog sdkbuild.Program, logger *zap.SugaredLogger, lang clioptions.Language, port int) (*exec.Cmd, error) {
var args []string
// Python needs module name
if lang == clioptions.LangPython {
switch lang {
case clioptions.LangPython:
// Python needs module name.
args = append(args, "main")
case clioptions.LangTypeScript:
// Node needs the compiled module before the harness subcommand.
args = append(args, "./tslib/main.js")
}
args = append(args, "project-server", "--port", strconv.Itoa(port))
cmd, err := prog.NewCommand(ctx, args...)
Expand Down
16 changes: 13 additions & 3 deletions scenarios/project/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ func TestValidateRequiresLanguage(t *testing.T) {
require.EqualError(t, err, "--option language=<lang> is required")
}

func TestValidateLimitedPythonSupport(t *testing.T) {
func TestValidateLimitedProjectLanguageSupport(t *testing.T) {
_, err := (&projectScenarioExecutor{}).validate(loadgen.ScenarioInfo{
ScenarioOptions: map[string]string{
"language": "go",
},
})
require.EqualError(t, err, "project scenario is currently limited to Python, got go")
require.EqualError(t, err, "project scenario is currently limited to Python and TypeScript, got go")
}

func TestValidateRejectsConflictingProjectSources(t *testing.T) {
Expand All @@ -55,6 +55,14 @@ func TestPythonHelloWorldPrebuilt(t *testing.T) {
runProjectScenario(t, "python", "helloworld", "", nil, true)
}

func TestTypeScriptHelloWorldSourceBuild(t *testing.T) {
runProjectScenario(t, "typescript", "helloworld", "", nil, false)
}

func TestTypeScriptHelloWorldPrebuilt(t *testing.T) {
runProjectScenario(t, "typescript", "helloworld", "", nil, true)
}

func runProjectScenario(
t *testing.T,
lang, projectName, version string,
Expand Down Expand Up @@ -88,7 +96,9 @@ func runProjectScenario(

scenarioErrCh := make(chan error, 1)
go func() {
scenarioErrCh <- (&projectScenarioExecutor{}).Run(ctx, info)
scenarioErrCh <- (&projectScenarioExecutor{
clientReadyTimeout: 30 * time.Second,
}).Run(ctx, info)
}()

select {
Expand Down
31 changes: 19 additions & 12 deletions workers/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ func (b *Builder) buildJava(ctx context.Context, baseDir string) (sdkbuild.Progr
}

func (b *Builder) buildTypeScript(ctx context.Context, baseDir string) (sdkbuild.Program, error) {
moreDependencies := map[string]string{
"@temporalio/omes-project-harness": "file:../harness",
"winston": "^3.11.0",
}
if b.ProjectName != "" {
// Project fixtures build from workers/typescript/projects/tests/<name>,
// so their generated package needs to walk back to workers/typescript/harness.
moreDependencies["@temporalio/omes-project-harness"] = "file:../../../../harness"
}

// If version not provided, try to read it from package.json
version := b.SdkOptions.Version
if version == "" {
Expand Down Expand Up @@ -190,18 +200,15 @@ func (b *Builder) buildTypeScript(ctx context.Context, baseDir string) (sdkbuild
}

prog, err := sdkbuild.BuildTypeScriptProgram(ctx, sdkbuild.BuildTypeScriptProgramOptions{
BaseDir: baseDir,
Version: version,
TSConfigPaths: map[string][]string{"@temporalio/omes": {"src/omes.ts"}},
DirName: b.DirName,
ApplyToCommand: nil,
Includes: []string{"../src/**/*.ts", "../src/protos/json-module.js", "../src/protos/root.js"},
MoreDependencies: map[string]string{
"@temporalio/omes-project-harness": "file:../harness",
"winston": "^3.11.0",
},
Stdout: b.stdout,
Stderr: b.stderr,
BaseDir: baseDir,
Version: version,
TSConfigPaths: map[string][]string{"@temporalio/omes": {"src/main.ts"}},
DirName: b.DirName,
ApplyToCommand: nil,
Includes: []string{"../src/**/*.ts", "../src/protos/json-module.js", "../src/protos/root.js"},
MoreDependencies: moreDependencies,
Stdout: b.stdout,
Stderr: b.stderr,
})
if err != nil {
return nil, fmt.Errorf("failed preparing: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion workers/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func (r *Runner) Run(ctx context.Context, baseDir string) error {
args = append(args, "main", "worker")
case clioptions.LangTypeScript:
// Node also needs module before the harness subcommand.
args = append(args, "./tslib/omes.js", "worker")
args = append(args, "./tslib/main.js", "worker")
case clioptions.LangDotNet, clioptions.LangRuby, clioptions.LangGo:
// .NET, Ruby, and Go just need the harness worker subcommand
args = append(args, "worker")
Expand Down
18 changes: 18 additions & 0 deletions workers/typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions workers/typescript/projects/tests/helloworld/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@temporalio/omes-project-helloworld",
"version": "0.1.0",
"private": true,
"main": "./dist/main.js",
"types": "./dist/main.d.ts",
"scripts": {
"build": "tsc --build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@temporalio/client": "1.15.0",
"@temporalio/omes-project-harness": "file:../../../harness",
"@temporalio/worker": "1.15.0"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.1.0",
"typescript": "^5.9.3"
}
}
46 changes: 46 additions & 0 deletions workers/typescript/projects/tests/helloworld/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Client } from '@temporalio/client';
import { NativeConnection, Worker } from '@temporalio/worker';
import {
defaultClientFactory,
type App,
type ProjectExecuteContext,
type WorkerContext,
} from '@temporalio/omes-project-harness';

export function app(): App {
return {
worker: buildWorker,
clientFactory: defaultClientFactory,
project: {
execute: executeProjectIteration,
},
};
}

async function buildWorker(client: Client, context: WorkerContext): Promise<Worker> {
const connection = client.connection;
if (!(connection instanceof NativeConnection)) {
throw new Error('Helloworld project requires a NativeConnection-backed client');
}

return await Worker.create({
connection,
namespace: client.options.namespace,
taskQueue: context.taskQueue,
workflowsPath: require.resolve('./workflow'),
...context.workerOptions,
});
}

async function executeProjectIteration(
client: Client,
context: ProjectExecuteContext,
): Promise<void> {
const handle = await client.workflow.start('helloWorldWorkflow', {
args: ['World'],
taskQueue: context.taskQueue,
workflowId: `${context.run.executionId}-${context.iteration}`,
});
const result = await handle.result();
console.log(result);
}
11 changes: 11 additions & 0 deletions workers/typescript/projects/tests/helloworld/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { run } from '@temporalio/omes-project-harness';
import { app } from './app';

async function main(): Promise<void> {
await run(app());
}

void main().catch((err: unknown) => {
console.error(err);
process.exit(1);
});
Loading
Loading