Skip to content

feat: deliver gated host configuration snapshots - #1927

Open
Rohit Ranjan (RohitRanjanMS) wants to merge 10 commits into
Azure:devfrom
RohitRanjanMS:rohitranjanms-python-host-configuration
Open

Rohit Ranjan (RohitRanjanMS) wants to merge 10 commits into
Azure:devfrom
RohitRanjanMS:rohitranjanms-python-host-configuration

Conversation

@RohitRanjanMS

@RohitRanjanMS Rohit Ranjan (RohitRanjanMS) commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Description

Relates to Azure/azure-functions-host#12049 (non-closing).

Port APP-01 Host Configuration delivery to Python 3.13+ proxy/R2P2, through both independently packaged v1/v2 library runtimes. The classic worker (workers/azure_functions_worker, Python <=3.12) is unchanged from the PR baseline and does not implement delivery. This reflects the explicitly approved scope reduction in 4a694c9dd78d2a10115c344f3cd5d04316139cb0; the earlier reply retaining classic coverage has been corrected in the review thread.

This is source implementation and validation, not a claim that existing published runtime wheels already contain the feature.

  • Gate exactly on WorkerInitRequest.capabilities["HostConfiguration"] == "1"; otherwise omit the field without obtaining the application root, reading a file, initializing the snapshot, or emitting delivery diagnostics.
  • Capture host.json beneath worker-local FUNCTIONS_APPLICATION_DIRECTORY, preserving explicitly configured relative paths and distinguishing a missing file at open from an unavailable/missing parent. No Host-supplied-directory, worker-directory, or implicit cwd fallback. Not-found errors during reading/closing are Error, not Missing.
  • Incrementally decode strict UTF-8 or BOM-marked UTF-8/UTF-16/UTF-32. Enforce the inclusive 65,536-byte cap on converted BOM-free UTF-8 before parsing, and again on normalized output. Oversized malformed JSON/comments therefore produce TooLarge; non-Available outcomes contain no bytes.
  • Normalize input to a strict compact JSON object, accepting/removing // and /* */ comments and trailing commas. Preserve raw number spellings, string/property escapes, duplicate keys and key order. Require exactly one object root and at most 64 nested containers. {} and nested arrays/scalars inside an object remain valid. Empty/BOM-only files, non-object roots (including after comments), malformed JSON and whitespace/comment-only input produce content-free Error/InvalidJson.
  • Cache success and failure outcomes once per process/application generation, replay through fresh envelopes, and serialize capture against the existing runtime specialization environment replacement. Emit exactly one content-free warning with a fixed reason code when a failure is cached; no warning on success, Missing, disabled calls or replay. No new environment-reload fields or reload policy; scope reduction leaves retained runtime lifecycle code unchanged.
  • Preserve existing initialization success/failure paths, capabilities and metadata. No app-setting merge, content/path-bearing error telemetry, public SDK/options API, hashing/comparison, or dependency upgrades. Host configuration interpretation and override precedence remain Host-owned.
  • Add canonical field 6 and the exact HostConfiguration message/comments to the proxy and R2P2 vendored schemas, proxy protobuf exports, R2P2's handwritten adapter, and its explicit Rust response mapping. v1/v2 distributions carry identical package-local helpers; shared tests prevent drift without importing classic code or another runtime distribution.

Reference pins

  • Python baseline: 8701c546f6bb4d4305d1f498f35f3ad1b5539351.
  • Current scope-reduction commit: 4a694c9dd78d2a10115c344f3cd5d04316139cb0, following object-root parity commit a8893673f1c98122a4a151e7ffcb87775b5d1c2f.
  • Canonical dependency: Azure/azure-functions-language-worker-protobuf#103, OPEN at 313666583af4e97f0b534f1cbf61a3812f277c12 when originally checked. Only APP-01 was vendored into the retained proxy/R2P2 schemas; unrelated upstream fields and local language options were not changed.
  • Behavioral parity: Azure/azure-functions-dotnet-worker#3522, HostConfigurationSnapshotProvider.cs, through e7765e7201008340e4cc407486bbd31649abf162. This adds empty/non-object rejection to normalization/diagnostic semantics from 4c440d8d38526a16eb549ac6fba1082fe5ad4450 (original delivery reference 6066d8c6597c8f9810a4467d10aea0ac4a58d62d).
  • Host receiver: Azure/azure-functions-host#12101, merged at 8da994a4e19324233a6f0efecea963a5cc5c5c03.

Normalization details: escaped lone/reversed surrogates remain unchanged; numeric tokens are never converted to Python integers/floats, avoiding digit limits, rounding and overflow (-0, 1E+2, 1e999, large/precise values). U+2028/U+2029 are invalid inside line comments or as JSON whitespace, but valid inside block comments and strings. Python's strict decoders accept legitimately encoded U+FFFD with or without a BOM; this intentionally does not copy the .NET StreamReader replacement-character workaround. The scope reduction changes no retained runtime normalization, dependencies or published pins.

Environment and reload boundaries

FUNCTIONS_APPLICATION_DIRECTORY is an ordinary process environment variable, not protected from customer Python code. A mutation after capture does not change the cached snapshot. Runtime environment reload clears/replaces os.environ from the incoming request and resets the snapshot under the capture lock; this variable is not exempted or preserved. Reload itself does not read or deliver configuration: its response has only the existing metadata/capabilities/result fields. A subsequent exact-capability initialization could capture the replacement root. This is not a new reload-delivery mechanism or a guarantee that Host would adopt a replacement snapshot; Host acceptance/lifetime policy is separate. The launcher must provision the root/mount before first enabled initialization—there is no worker readiness barrier or retry.

Validation performed for the Python 3.13+ scope reduction

Executed using local Python 3.14.3 (.venv314) for retained runtime/bridge behavior and Python 3.11.9 (.venv) only for classic baseline regressions, generation and lint. Test commands set $env:PYTHONDONTWRITEBYTECODE='1'. No dependencies were installed and no tests/CI gates were weakened.

From the repository root:

.\.venv314\Scripts\python.exe -m pytest -q -p no:cacheprovider workers\tests\unittest_r2p2\test_host_configuration.py workers\tests\unittest_r2p2\test_bridge_runtime.py workers\tests\unittest_r2p2\test_protos_adapter.py -k host_configuration

Passed: 600 tests, 34 deselected. The count is lower solely because the classic helper parameter was removed; all existing v1/v2 cases remain. These exercise the Python source runtimes/bridge/adapter, not the native Rust binary. Coverage includes exact gating/no IO, all encodings/BOM-only failures, object-root validation, raw tokens/duplicates, U+FFFD, exact byte limits, depth64, cached/concurrent outcomes, warnings/redaction, fresh envelopes and unchanged init results.

From each of runtimes\v1 and runtimes\v2, with $env:PYTHONPATH=(Get-Location).Path:

..\..\.venv314\Scripts\python.exe -m pytest -q -p no:cacheprovider --disable-warnings tests\unittests\test_host_configuration.py tests\unittests\test_handle_event.py tests\unittests\test_opentelemetry.py -k 'not environment_reload'

Passed: v1 35 tests, 2 deselected, 11 warnings; v2 43 tests, 4 deselected, 11 warnings. Feature specialization tests run with their existing process-local Windows time.tzset mocks. Existing tests named environment_reload are outside this selector; no lifecycle implementation changed in this follow-up and these results are not native Linux evidence.

From workers, with $env:PYTHONPATH=(Get-Location).Path:

..\.venv314\Scripts\python.exe -m pytest -q -p no:cacheprovider --disable-warnings tests\unittest_proxy\test_dispatcher.py
..\.venv\Scripts\python.exe -m pytest -q -p no:cacheprovider tests\unittests\test_dispatcher.py tests\unittests\test_opentelemetry.py -k 'initialize_worker or init_request'

Passed: proxy 37 tests, 6 existing warnings; classic baseline regressions 18 tests, 9 pre-existing Python-version-specific skips, 118 deselected. Classic feature-specific tests were removed with the unsupported implementation; no pre-existing classic tests were removed. The classic source and unit-test tree exactly match the baseline.

From workers\tests:

..\..\.venv\Scripts\python.exe -m invoke -c test_setup build-protos

Passed. Regenerated ignored local classic bindings from the restored baseline schema using the existing generator; no generated output was hand-edited. This also cleaned stale worker build output before packaging. The classic generated descriptor has no HostConfiguration type/field; the unchanged proxy descriptor retains field 6.

From the repository root:

.\.venv\Scripts\python.exe -m flake8 --config .github\linters\tox.ini workers\azure_functions_worker\dispatcher.py workers\azure_functions_worker\protos\__init__.py workers\tests\unittest_r2p2\test_host_configuration.py
git -c core.longpaths=true --no-pager diff --check
git -c core.longpaths=true --no-pager diff --exit-code 8701c546f6bb4d4305d1f498f35f3ad1b5539351 -- workers\azure_functions_worker workers\tests\unittests

All passed. An initial flake8 invocation without the repository configuration reported baseline __init__.py re-export F401s; the repository-configured check above passes without changing those exports or the lint policy. Retained runtime/proxy/R2P2 product files are unchanged by this scope-reduction commit. The resulting full PR diff contains 20 files and none under the classic worker or classic unit-test tree.

Packaging and local wheel verification, from the repository root (session artifacts, no publication/pin changes):

.\.venv314\Scripts\python.exe -m pip wheel --quiet --no-deps --no-build-isolation --wheel-dir 'C:\Users\roranjan\.copilot\session-state\82b6de1f-4572-4dcf-ac78-4ad4b21474a4\files\host-config-proxy-only-wheels' .\workers .\runtimes\v1 .\runtimes\v2
.\.venv314\Scripts\python.exe -B 'C:\Users\roranjan\.copilot\session-state\82b6de1f-4572-4dcf-ac78-4ad4b21474a4\files\host-config-proxy-only-wheel-smoke.py'

Both passed. Three wheels built. The session-artifact smoke script confirms the worker wheel lacks the classic helper/message/response field while retaining proxy field 6. Both runtime-wheel helpers match source exactly; disabled no-capture plus 16 cached-delivery/warning/envelope cases per runtime (32 total) pass. A process-local probe using each runtime's actual sync executor also confirms customer os.environ mutation is visible but leaves cached delivery unchanged until reset. This does not simulate Host reload/adoption. These are locally built source wheels, not updated published artifacts.

Integration evidence and known validation gaps

The coordinator's earlier python worker-config-e2e.py --languages python run passed seven cases using the classic worker and real native Windows Host/C# WorkerProxy at Host 8da994a4e19324233a6f0efecea963a5cc5c5c03 (evidence: parent files\worker-config-e2e-20261103-112110\results.json). That classic delivery implementation has now been removed. Those historical results are not evidence for the current Python 3.13+ proxy/R2P2 scope. No new real Host/WorkerProxy integration or mount-readiness test was run for this revision.

The retained runtime test protobuf fixtures were originally regenerated using grpc_tools.protoc --python_out and test_setup.make_absolute_imports, preserving local language options. Each has 194 added / 179 removed lines from compiler header/runtime checks and serialized descriptor offsets; prior descriptor comparison confirmed pre-existing schema unchanged. Only the proxy/R2P2 vendored schemas now carry APP-01.

Native Rust/Python 3.15 build/tests/rustfmt/clippy remain unverified locally (cargo is unavailable). The adapter-to-prost round-trip test is authored but has not executed here. Native Linux validation of this revision remains outstanding.

CI infrastructure blocker is separate from product scope. Azure Pipelines build 313889, on the prior HEAD's merge commit, failed before R2P2 compilation because the approved Cargo feed returned HTTP 401 for uncached portable-atomic; the v2 job failed before tests when Python 3.13 download returned HTTP 504. Cargo/dependency/feed/job configuration is unchanged by this PR. The user chose to leave this for a pipeline/feed maintainer to repair access and rerun. No feed bypass, dependency workaround, extra trigger comment or CI waiver was added. This description does not claim CI on the newly pushed HEAD has passed.

Release/integration prerequisites

  • Launcher/provisioning must explicitly supply worker-local FUNCTIONS_APPLICATION_DIRECTORY and establish root/mount readiness before first enabled init; Python does not provide either guarantee.
  • The canonical protocol dependency remains an open PR, not a released protocol pin.
  • Publish v1/v2 runtime wheels containing this feature and coordinate their pin rollout before declaring shipped proxy/R2P2 artifacts feature-complete. Existing pins and release mechanisms are unchanged:
    • workers/pyproject.toml:42-43: runtime v2 1.3.0, runtime v1 1.2.0.
    • eng/pack/scripts/r2p2_deps.sh:169-170 and eng/pack/scripts/r2p2_deps.ps1:156-157: runtime v2 1.2.1, runtime v1 1.1.0.
    • R2P2's native-invocation/executor overlays do not install the new initialization handlers or snapshot helper. This PR does not invent versions or overlay incompatible handlers into older packages. Local verification uses source-built runtime wheels.

Pull Request Checklist

Host-Worker Contract

  • Does this PR impact the host-worker contract (e.g., gRPC messages, shared interfaces)?
    • If yes, have the changes been applied to:
      • azure_functions_worker (Python <= 3.12)
      • proxy_worker (Python 3.13-3.14)
      • r2p2 (Python >= 3.15)
    • If no, please explain why: Classic is intentionally unchanged and excluded by the approved Python 3.13+ BYOC scope. Canonical APP-01 schema/mapping changes apply only to proxy/R2P2. Native R2P2 validation remains outstanding.

Worker Execution Logic

  • Does this PR affect worker execution logic (e.g., function invocation, bindings, lifecycle)?
    If yes, please answer the following:

Python Version Coverage

  • Does this change apply to all supported worker models?
  • If yes, have the changes been made to:
    • azure_functions_worker (Python <= 3.12)
    • proxy_worker (Python 3.13-3.14)
    • r2p2 (Python >= 3.15)
    • runtimes/v1 / runtimes/v2 (Python >= 3.13)
  • If no, please explain why: Delivery is intentionally limited to Python 3.13+ proxy/R2P2 and their v1/v2 runtimes. Classic source and existing unit tests match the baseline. Retained feature execution was validated locally on Python 3.14; runtime publication and native Rust/Python 3.15 validation are outstanding.

Programming Model Compatibility (proxy_worker and r2p2)

  • Does this change apply to both:
    • V1 programming model (runtimes/v1)?
    • V2 programming model (runtimes/v2)?
  • Explanation (if limited to one model): N/A; both source runtimes remain covered, with published-runtime rollout required as described above.

Add APP-01 snapshot delivery to classic and library runtimes, canonical protocol fields, and the R2P2 adapter/codec without changing initialization outcomes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Port token-preserving JSONC normalization and cached fixed-reason diagnostics from dotnet-worker commit 4c440d8d38526a16eb549ac6fba1082fe5ad4450. Preserve strict decoding, byte limits, capability gating and initialization outcomes across classic and library runtimes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-language startup and native response codec changes require maintainer validation because Rust/PyO3, Python 3.15, and native Linux checks remain outstanding, while Host integration evidence predates the normalization changes.

No open issues 🎉

2 files not reviewed
  • runtimes/v1/tests/protos/FunctionRpc_pb2.py: Generated file
  • runtimes/v2/tests/protos/FunctionRpc_pb2.py: Generated file
What changed in this PR

Adds capability-gated host.json snapshot delivery across the classic Python worker, both library runtimes, and R2P2, while leaving configuration interpretation and acceptance to the Host.

Changes:

  • Adds bounded decoding, JSONC normalization, generation-scoped caching, and content-free failure diagnostics.
  • Extends all three protocol schemas, Python exports, and R2P2 adapters to transport the optional snapshot.
  • Adds contract, initialization, specialization, and serialization tests, plus integration and release documentation.

This is a source implementation; runtime-wheel publication and pin rollout remain prerequisites. Native Rust, Linux, and Python 3.15 validation remain outstanding, as documented.

File Description
workers/​tests/​unittests/​test_host_configuration.py Tests classic initialization delivery and specialization reset.
workers/​tests/​unittest_r2p2/​test_protos_adapter.py Tests snapshot status, bytes, and field presence.
workers/​tests/​unittest_r2p2/​test_host_configuration.py Adds shared helper contract tests and drift prevention.
workers/​tests/​unittest_r2p2/​test_bridge_runtime.py Tests source-runtime delivery through the Python bridge.
workers/​tests/​unittest_proxy/​test_dispatcher.py Tests opaque forwarding through proxy envelopes.
workers/​r2p2/​src/​control.rs Maps snapshots to Rust protobuf messages and adds round-trip coverage.
workers/​r2p2/​proto/​FunctionRpc.proto Adds the snapshot message and response field 6.
workers/​r2p2/​bridge/​protos_adapter.py Adds snapshot representation and optional serialization.
workers/​proxy_worker/​protos/​_src/​src/​proto/​FunctionRpc.proto Adds the canonical snapshot contract for proxy transport.
workers/​proxy_worker/​protos/​__init__.py Exports HostConfiguration.
workers/​azure_functions_worker/​protos/​_src/​src/​proto/​FunctionRpc.proto Adds the canonical snapshot contract for classic transport.
workers/​azure_functions_worker/​protos/​__init__.py Exports HostConfiguration.
workers/​azure_functions_worker/​host_configuration.py Implements gated capture, normalization, caching, and diagnostics.
workers/​azure_functions_worker/​dispatcher.py Attaches snapshots during initialization and resets on specialization.
runtimes/​v2/​tests/​unittests/​test_host_configuration.py Tests v2 delivery, initialization failures, and generation reset.
runtimes/​v2/​tests/​protos/​FunctionRpc_pb2.py Regenerates v2 protocol fixtures with snapshot support.
runtimes/​v2/​tests/​protos/​__init__.py Exports the snapshot test message.
runtimes/​v2/​azure_functions_runtime/​host_configuration.py Adds the independently packaged v2 snapshot helper.
runtimes/​v2/​azure_functions_runtime/​handle_event.py Integrates snapshots into v2 initialization and specialization.
runtimes/​v1/​tests/​unittests/​test_host_configuration.py Tests v1 delivery, initialization errors, and generation reset.
runtimes/​v1/​tests/​protos/​FunctionRpc_pb2.py Regenerates v1 protocol fixtures with snapshot support.
runtimes/​v1/​tests/​protos/​__init__.py Exports the snapshot test message.
runtimes/​v1/​azure_functions_runtime_v1/​host_configuration.py Adds the independently packaged v1 snapshot helper.
runtimes/​v1/​azure_functions_runtime_v1/​handle_event.py Integrates snapshots into v1 initialization and specialization.
README.md Documents delivery behavior, protocol generation, and rollout prerequisites.

Review effort: Balanced

@gavin-aguiar

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Match dotnet-worker e7765e7 by rejecting decoded empty and non-object JSON with cached InvalidJson outcomes. Preserve token normalization, strict decoding, byte/depth limits and existing initialization behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@hallvictoria

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Restore the classic worker and unit-test tree to the baseline. Retain proxy/R2P2 delivery through the v1/v2 runtime helpers, scope shared tests accordingly, and clarify process-environment and reload behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@gavin-aguiar

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Preserve both the host-configuration envelope tests and the incoming BYOC connection tests in the proxy dispatcher suite.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread README.md Resolved
hallvictoria and others added 3 commits November 5, 2026 13:51
Keep implementation and rollout details in the pull request description rather than the customer-facing repository overview.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@hallvictoria

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Apply the rustfmt layout required by the R2P2 CI formatting check without changing test tokens or behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@hallvictoria

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants