Skip to content
Merged
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
24 changes: 24 additions & 0 deletions azure/durable_functions/models/DurableOrchestrationContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ def __init__(self,
self.open_tasks = defaultdict(list)
self.deferred_tasks: Dict[Union[int, str], Tuple[HistoryEvent, bool, str]] = {}

self._version: str = self._extract_version_from_history(self._histories)

@classmethod
def from_json(cls, json_string: str):
"""Convert the value passed into a new instance of the class.
Expand Down Expand Up @@ -752,3 +754,25 @@ def _get_function_name(self, name: FunctionBuilder,
"https://github.com/Azure/azure-functions-durable-python.\n"\
"Error trace: " + e.message
raise e

@property
def version(self) -> Optional[str]:
"""Get the version assigned to the orchestration instance on creation.

Returns
-------
Optional[str]
The version assigned to the orchestration instance on creation, or None if not found.
"""
return self._version

@staticmethod
def _extract_version_from_history(history_events: List[HistoryEvent]) -> Optional[str]:
"""Extract the version from the execution started event in history.

Returns None if not found.
"""
for event in history_events:
if event.event_type == HistoryEventType.EXECUTION_STARTED:
return event.Version
return None
5 changes: 5 additions & 0 deletions samples-v2/orchestration_versioning/.funcignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.git*
.vscode
local.settings.json
test
.venv
130 changes: 130 additions & 0 deletions samples-v2/orchestration_versioning/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don’t work, or not
# install all needed dependencies.
#Pipfile.lock

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# Azure Functions artifacts
bin
obj
appsettings.json
local.settings.json
.python_packages
44 changes: 44 additions & 0 deletions samples-v2/orchestration_versioning/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Versioning

This directory contains a Function app that demonstrates how to make changes to an orchestrator function without breaking existing orchestration instances.

The orchestrator function has two code paths:

1. The old path invoking `activity_a`.
2. The new path invoking `activity_b` instead.

While `defaultVersion` in `host.json` is set to `1.0`, the orchestrator will always follow the first path, producing the following output:

```
Orchestration version: 1.0
Suborchestration version: 1.0
Hello from A!
```

When `defaultVersion` in `host.json` is updated (for example, to `2.0`), *new orchestration instances* will follow the new path, producing the following output:

```
Orchestration version: 2.0
Suborchestration version: 2.0
Hello from B!
```

What happens to *existing orchestration instances* that were started *before* the `defaultVersion` change? Waiting for an external event in the middle of the orchestrator provides a convenient opportunity to emulate a deployment while orchestration instances are still running:

1. Create a new orchestration by invoking the HTTP trigger (`http_start`).
2. Wait for the orchestration to reach the point where it is waiting for an external event.
3. Stop the app.
4. Change `defaultVersion` in `host.json` to `2.0`.
5. Deploy and start the updated app.
6. Trigger the external event.
7. Observe that the orchestration output.

```
Orchestration version: 1.0
Suborchestration version: 2.0
Hello from A!
```

Note that the value returned by `context.version` is permanently associated with the orchestrator instance and is not impacted by the `defaultVersion` change. As a result, the orchestrator follows the old execution path to guarantee deterministic replay behavior.

However, the suborchestration version is `2.0` because this suborchestration was created *after* the `defaultVersion` change.
46 changes: 46 additions & 0 deletions samples-v2/orchestration_versioning/function_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import logging
import azure.functions as func
import azure.durable_functions as df

myApp = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)

@myApp.route(route="orchestrators/{functionName}")
@myApp.durable_client_input(client_name="client")
async def http_start(req: func.HttpRequest, client):
function_name = req.route_params.get('functionName')
instance_id = await client.start_new(function_name)

logging.info(f"Started orchestration with ID = '{instance_id}'.")
return client.create_check_status_response(req, instance_id)

@myApp.orchestration_trigger(context_name="context")
def my_orchestrator(context: df.DurableOrchestrationContext):
# context.version contains the value of defaultVersion in host.json
# at the moment when the orchestration was created.
if (context.version == "1.0"):
# Legacy code path
activity_result = yield context.call_activity('activity_a')
else:
# New code path
activity_result = yield context.call_activity('activity_b')

# Provide an opportunity to update and restart the app
context.set_custom_status("Waiting for Continue event...")
yield context.wait_for_external_event("Continue")
context.set_custom_status("Continue event received")

# New sub-orchestrations will use the current defaultVersion specified in host.json
sub_result = yield context.call_sub_orchestrator('my_sub_orchestrator')
return [f'Orchestration version: {context.version}', f'Suborchestration version: {sub_result}', activity_result]

@myApp.orchestration_trigger(context_name="context")
def my_sub_orchestrator(context: df.DurableOrchestrationContext):
return context.version

@myApp.activity_trigger()
def activity_a() -> str:
return f"Hello from A!"

@myApp.activity_trigger()
def activity_b() -> str:
return f"Hello from B!"
16 changes: 16 additions & 0 deletions samples-v2/orchestration_versioning/host.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensions": {
"durableTask": {
"defaultVersion": "1.0"
}
}
}
7 changes: 7 additions & 0 deletions samples-v2/orchestration_versioning/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# DO NOT include azure-functions-worker in this file
# The Python Worker is managed by Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues

azure-functions
azure-functions-durable
pytest
8 changes: 8 additions & 0 deletions tests/models/test_DurableOrchestrationContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,11 @@ def test_get_input_json_str():
result = context.get_input()

assert 'Seattle' == result['city']

def test_version_equals_version_from_execution_started_event():
builder = ContextBuilder('test_function_context')
builder.history_events = []
builder.add_orchestrator_started_event()
builder.add_execution_started_event(name="TestOrchestrator", version="1.0")
context = DurableOrchestrationContext.from_json(builder.to_json_string())
assert context.version == "1.0"