Skip to content

Commit 794f0b8

Browse files
authored
Add Worker Deployment Versioning sample (#48)
1 parent 5955719 commit 794f0b8

10 files changed

Lines changed: 539 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.bundle
22
.vscode
3+
.claude
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
require 'test'
4+
require 'worker_versioning/app'
5+
require 'worker_versioning/workerv1'
6+
require 'worker_versioning/workerv1_1'
7+
require 'worker_versioning/workerv2'
8+
require 'temporalio/testing'
9+
10+
module WorkerVersioning
11+
class WorkerVersioningTest < Test
12+
def test_worker_versioning_sample_can_run
13+
skip_if_not_x86!
14+
15+
Temporalio::Testing::WorkflowEnvironment.start_local do |env|
16+
worker_v1_task = Thread.new { WorkerVersioning::WorkerV1.run_async(env.client) }
17+
worker_v1_1_task = Thread.new { WorkerVersioning::WorkerV1Dot1.run_async(env.client) }
18+
worker_v2_task = Thread.new { WorkerVersioning::WorkerV2.run_async(env.client) }
19+
20+
begin
21+
main(env.client)
22+
assert(true, 'Worker versioning demo completed successfully')
23+
ensure
24+
[worker_v1_task, worker_v1_1_task, worker_v2_task].each(&:kill)
25+
end
26+
end
27+
end
28+
end
29+
end

worker_versioning/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## Worker Versioning
2+
3+
This sample demonstrates how to use Temporal's Worker Versioning feature to safely deploy updates to workflow and activity code. It shows the difference between auto-upgrading and pinned workflows, and how to manage worker deployments with different build IDs.
4+
5+
The sample creates multiple worker versions (1.0, 1.1, and 2.0) within one deployment and demonstrates:
6+
- **Auto-upgrading workflows**: Automatically and controllably migrate to newer worker versions
7+
- **Pinned workflows**: Stay on the original worker version throughout their lifecycle
8+
- **Compatible vs incompatible changes**: How to make safe updates using `Workflow.patched`
9+
10+
### Steps to run this sample:
11+
12+
1) Run a [Temporal service](https://github.com/temporalio/samples-ruby/tree/main/#how-to-use).
13+
Ensure that you're using at least Server version 1.28.0 (CLI version 1.4.0).
14+
15+
2) Start the main application (this will guide you through the sample):
16+
```bash
17+
ruby worker_versioning/app.rb
18+
```
19+
20+
3) Follow the prompts to start workers in separate terminals:
21+
- When prompted, run: `ruby worker_versioning/workerv1.rb`
22+
- When prompted, run: `ruby worker_versioning/workerv1_1.rb`
23+
- When prompted, run: `ruby worker_versioning/workerv2.rb`
24+
25+
The sample will show how auto-upgrading workflows migrate to newer workers while pinned workflows
26+
remain on their original version.

worker_versioning/activities.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
require 'temporalio/activity'
4+
5+
module WorkerVersioning
6+
module Activities
7+
class SomeActivity < Temporalio::Activity::Definition
8+
def execute(called_by)
9+
"some_activity called by #{called_by}"
10+
end
11+
end
12+
13+
class SomeIncompatibleActivity < Temporalio::Activity::Definition
14+
def execute(input_data)
15+
"some_incompatible_activity called by #{input_data['called_by']} with #{input_data['more_data']}"
16+
end
17+
end
18+
end
19+
end

worker_versioning/app.rb

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# frozen_string_literal: true
2+
3+
require 'temporalio/client'
4+
require 'temporalio/api/workflowservice/v1/request_response'
5+
require 'temporalio/worker_deployment_version'
6+
require 'logger'
7+
require 'securerandom'
8+
require_relative 'constants'
9+
10+
def main(client = nil)
11+
logger = Logger.new($stdout, level: Logger::INFO)
12+
13+
client ||= Temporalio::Client.connect(
14+
'localhost:7233',
15+
'default',
16+
logger:
17+
)
18+
19+
# Wait for v1 worker and set as current version
20+
logger.info(
21+
'Waiting for v1 worker to appear. Run `ruby worker_versioning/workerv1.rb` in another terminal'
22+
)
23+
wait_for_worker_and_make_current(client, '1.0')
24+
25+
# Start auto-upgrading and pinned workflows. Importantly, note that when we start the workflows,
26+
# we are using a workflow type name which does *not* include the version number. We defined them
27+
# with versioned names so we could show changes to the code, but here when the client invokes
28+
# them, we're demonstrating that the client remains version-agnostic.
29+
auto_upgrade_workflow_id = "worker-versioning-versioning-autoupgrade_#{SecureRandom.uuid}"
30+
auto_upgrade_execution = client.start_workflow(
31+
:AutoUpgrading,
32+
id: auto_upgrade_workflow_id,
33+
task_queue: WorkerVersioning::Constants::TASK_QUEUE
34+
)
35+
36+
pinned_workflow_id = "worker-versioning-versioning-pinned_#{SecureRandom.uuid}"
37+
pinned_execution = client.start_workflow(
38+
:Pinned,
39+
id: pinned_workflow_id,
40+
task_queue: WorkerVersioning::Constants::TASK_QUEUE
41+
)
42+
43+
logger.info("Started auto-upgrading workflow: #{auto_upgrade_execution.id}")
44+
logger.info("Started pinned workflow: #{pinned_execution.id}")
45+
46+
# Signal both workflows a few times to drive them
47+
advance_workflows(auto_upgrade_execution, pinned_execution)
48+
49+
# Now wait for the v1.1 worker to appear and become current
50+
logger.info(
51+
'Waiting for v1.1 worker to appear. Run `ruby worker_versioning/workerv1_1.rb` in another terminal'
52+
)
53+
wait_for_worker_and_make_current(client, '1.1')
54+
55+
# Once it has, we will continue to advance the workflows.
56+
# The auto-upgrade workflow will now make progress on the new worker, while the pinned one will
57+
# keep progressing on the old worker.
58+
advance_workflows(auto_upgrade_execution, pinned_execution)
59+
60+
# Finally we'll start the v2 worker, and again it'll become the new current version
61+
logger.info(
62+
'Waiting for v2 worker to appear. Run `ruby worker_versioning/workerv2.rb` in another terminal'
63+
)
64+
wait_for_worker_and_make_current(client, '2.0')
65+
66+
# Once it has we'll start one more new workflow, another pinned one, to demonstrate that new
67+
# pinned workflows start on the current version.
68+
pinned_workflow_2_id = "worker-versioning-versioning-pinned-2_#{SecureRandom.uuid}"
69+
pinned_execution_v2 = client.start_workflow(
70+
:Pinned,
71+
id: pinned_workflow_2_id,
72+
task_queue: WorkerVersioning::Constants::TASK_QUEUE
73+
)
74+
logger.info("Started pinned workflow v2: #{pinned_execution_v2.id}")
75+
76+
# Now we'll conclude all workflows. You should be able to see in your server UI that the pinned
77+
# workflow always stayed on 1.0, while the auto-upgrading workflow migrated.
78+
[auto_upgrade_execution, pinned_execution, pinned_execution_v2].each do |handle|
79+
handle.signal(:do_next_signal, 'conclude')
80+
handle.result
81+
end
82+
83+
logger.info('All workflows completed')
84+
end
85+
86+
def advance_workflows(auto_upgrade_execution, pinned_execution)
87+
# Signal both workflows a few times to drive them.
88+
3.times do
89+
auto_upgrade_execution.signal(:do_next_signal, 'do-activity')
90+
pinned_execution.signal(:do_next_signal, 'some-signal')
91+
end
92+
end
93+
94+
def wait_for_worker_and_make_current(client, build_id)
95+
target_version = Temporalio::WorkerDeploymentVersion.new(
96+
deployment_name: WorkerVersioning::Constants::DEPLOYMENT_NAME,
97+
build_id: build_id
98+
)
99+
100+
loop do
101+
describe_request = Temporalio::Api::WorkflowService::V1::DescribeWorkerDeploymentRequest.new(
102+
namespace: client.namespace,
103+
deployment_name: WorkerVersioning::Constants::DEPLOYMENT_NAME
104+
)
105+
response = client.workflow_service.describe_worker_deployment(describe_request)
106+
107+
found = response.worker_deployment_info.version_summaries.any? do |version_summary|
108+
version_summary.deployment_version.deployment_name == target_version.deployment_name &&
109+
version_summary.deployment_version.build_id == target_version.build_id
110+
end
111+
112+
break if found
113+
114+
sleep(1)
115+
rescue Temporalio::Error::RPCError => e
116+
# If not-found, wait a second and try again
117+
raise unless e.code == Temporalio::Error::RPCError::Code::NOT_FOUND
118+
119+
sleep(1)
120+
next
121+
end
122+
123+
# Once the version is available, set it as current
124+
set_request = Temporalio::Api::WorkflowService::V1::SetWorkerDeploymentCurrentVersionRequest.new(
125+
namespace: client.namespace,
126+
deployment_name: WorkerVersioning::Constants::DEPLOYMENT_NAME,
127+
version: target_version.to_canonical_string
128+
)
129+
client.workflow_service.set_worker_deployment_current_version(set_request)
130+
end
131+
132+
main if __FILE__ == $PROGRAM_NAME

worker_versioning/constants.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
module WorkerVersioning
4+
module Constants
5+
TASK_QUEUE = 'worker-versioning'
6+
DEPLOYMENT_NAME = 'my-deployment'
7+
end
8+
end

worker_versioning/workerv1.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'workflows'
4+
require_relative 'activities'
5+
require_relative 'constants'
6+
require 'logger'
7+
require 'temporalio/client'
8+
require 'temporalio/worker'
9+
require 'temporalio/worker/deployment_options'
10+
require 'temporalio/worker_deployment_version'
11+
12+
module WorkerVersioning
13+
module WorkerV1
14+
def self.run_async(client)
15+
worker = Temporalio::Worker.new(
16+
client: client,
17+
task_queue: WorkerVersioning::Constants::TASK_QUEUE,
18+
workflows: [WorkerVersioning::Workflows::AutoUpgradingWorkflowV1,
19+
WorkerVersioning::Workflows::PinnedWorkflowV1],
20+
activities: [WorkerVersioning::Activities::SomeActivity,
21+
WorkerVersioning::Activities::SomeIncompatibleActivity],
22+
deployment_options: Temporalio::Worker::DeploymentOptions.new(
23+
version: Temporalio::WorkerDeploymentVersion.new(
24+
deployment_name: WorkerVersioning::Constants::DEPLOYMENT_NAME,
25+
build_id: '1.0'
26+
),
27+
use_worker_versioning: true
28+
)
29+
)
30+
worker.run
31+
end
32+
end
33+
end
34+
35+
if __FILE__ == $PROGRAM_NAME
36+
logger = Logger.new($stdout, level: Logger::INFO)
37+
38+
client = Temporalio::Client.connect(
39+
'localhost:7233',
40+
'default',
41+
logger: logger
42+
)
43+
44+
logger.info('Starting worker v1 (build 1.0)')
45+
WorkerVersioning::WorkerV1.run_async(client)
46+
end

worker_versioning/workerv1_1.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'workflows'
4+
require_relative 'activities'
5+
require_relative 'constants'
6+
require 'logger'
7+
require 'temporalio/client'
8+
require 'temporalio/worker'
9+
require 'temporalio/worker/deployment_options'
10+
require 'temporalio/worker_deployment_version'
11+
12+
module WorkerVersioning
13+
module WorkerV1Dot1
14+
def self.run_async(client)
15+
worker = Temporalio::Worker.new(
16+
client: client,
17+
task_queue: WorkerVersioning::Constants::TASK_QUEUE,
18+
workflows: [WorkerVersioning::Workflows::AutoUpgradingWorkflowV1b,
19+
WorkerVersioning::Workflows::PinnedWorkflowV1],
20+
activities: [WorkerVersioning::Activities::SomeActivity,
21+
WorkerVersioning::Activities::SomeIncompatibleActivity],
22+
deployment_options: Temporalio::Worker::DeploymentOptions.new(
23+
version: Temporalio::WorkerDeploymentVersion.new(
24+
deployment_name: WorkerVersioning::Constants::DEPLOYMENT_NAME,
25+
build_id: '1.1'
26+
),
27+
use_worker_versioning: true
28+
)
29+
)
30+
worker.run
31+
end
32+
end
33+
end
34+
35+
if __FILE__ == $PROGRAM_NAME
36+
logger = Logger.new($stdout, level: Logger::INFO)
37+
38+
client = Temporalio::Client.connect(
39+
'localhost:7233',
40+
'default',
41+
logger: logger
42+
)
43+
44+
logger.info('Starting worker v1.1 (build 1.1)')
45+
WorkerVersioning::WorkerV1Dot1.run_async(client)
46+
end

worker_versioning/workerv2.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'workflows'
4+
require_relative 'activities'
5+
require_relative 'constants'
6+
require 'logger'
7+
require 'temporalio/client'
8+
require 'temporalio/worker'
9+
require 'temporalio/worker/deployment_options'
10+
require 'temporalio/worker_deployment_version'
11+
12+
module WorkerVersioning
13+
module WorkerV2
14+
def self.run_async(client)
15+
worker = Temporalio::Worker.new(
16+
client: client,
17+
task_queue: WorkerVersioning::Constants::TASK_QUEUE,
18+
workflows: [WorkerVersioning::Workflows::AutoUpgradingWorkflowV1b,
19+
WorkerVersioning::Workflows::PinnedWorkflowV2],
20+
activities: [WorkerVersioning::Activities::SomeActivity,
21+
WorkerVersioning::Activities::SomeIncompatibleActivity],
22+
deployment_options: Temporalio::Worker::DeploymentOptions.new(
23+
version: Temporalio::WorkerDeploymentVersion.new(
24+
deployment_name: WorkerVersioning::Constants::DEPLOYMENT_NAME,
25+
build_id: '2.0'
26+
),
27+
use_worker_versioning: true
28+
)
29+
)
30+
worker.run
31+
end
32+
end
33+
end
34+
35+
if __FILE__ == $PROGRAM_NAME
36+
logger = Logger.new($stdout, level: Logger::INFO)
37+
38+
client = Temporalio::Client.connect(
39+
'localhost:7233',
40+
'default',
41+
logger: logger
42+
)
43+
44+
logger.info('Starting worker v2 (build 2.0)')
45+
WorkerVersioning::WorkerV2.run_async(client)
46+
end

0 commit comments

Comments
 (0)