Skip to content

Commit 83436cd

Browse files
committed
Merge remote-tracking branch 'upstream/dev'
2 parents 82cedee + 76a2b12 commit 83436cd

File tree

58 files changed

+3169
-833
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+3169
-833
lines changed

azure-pipelines-1.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Starter pipeline
2+
# Start with a minimal pipeline that you can customize to build and deploy your code.
3+
# Add steps that build, run tests, deploy, and more:
4+
# https://aka.ms/yaml
5+
6+
#trigger:
7+
#- master
8+
#- dev
9+
10+
pool:
11+
vmImage: 'vs2017-win2016'
12+
13+
variables:
14+
Configuration: Release
15+
16+
steps:
17+
- pwsh: ./build.ps1 -NoBuild -Bootstrap
18+
displayName: 'Running ./build.ps1 -NoBuild -Bootstrap'
19+
20+
- pwsh: |
21+
$ErrorActionPreference = "Stop"
22+
./build.ps1 -Clean -Configuration Release
23+
displayName: './build.ps1 -Clean -Configuration Release'
24+
25+
- pwsh: ./test/E2E/Start-E2ETest.ps1
26+
env:
27+
AzureWebJobsStorage: $(AzureWebJobsStorage)
28+
AzureWebJobsCosmosDBConnectionString: $(AzureWebJobsCosmosDBConnectionString)
29+
AzureWebJobsServiceBus: $(AzureWebJobsServiceBus)
30+
AzureWebJobsEventHubSender: $(AzureWebJobsEventHubSender)
31+
FUNCTIONS_WORKER_RUNTIME : "powershell"
32+
continueOnError: true
33+
displayName: 'Running E2ETest'
34+
35+
- pwsh: |
36+
$null = New-Item -Path ./E2ETestArtifacts -ItemType Directory -Force
37+
Compress-Archive -Path ./src/bin/Release/netcoreapp2.2/publish/* -DestinationPath ./E2ETestArtifacts/powershellworker.zip -Verbose
38+
Compress-Archive -Path ./test/E2E/TestFunctionApp/* -DestinationPath ./E2ETestArtifacts/e2etestspowershell.zip -Verbose
39+
displayName: 'Create test app zip file'
40+
41+
- pwsh: |
42+
if (-not (Get-command new-azstoragecontext -ea SilentlyContinue))
43+
{
44+
Install-Module Az.Storage -Force -Verbose -Scope CurrentUser
45+
}
46+
47+
write-host "Creating context"
48+
$context = New-AzStorageContext -StorageAccountName $(StorageAccountName) `
49+
-StorageAccountKey $(StorageAccountKey) `
50+
-Verbose
51+
52+
foreach ($fileName in @("e2etestspowershell.zip", "powershellworker.zip"))
53+
{
54+
write-host "Uploading file to Azure Blob"
55+
Set-AzStorageBlobContent -File ./E2ETestArtifacts/$fileName `
56+
-Container $(ContainerName) `
57+
-Blob $fileName `
58+
-Context $context `
59+
-Force `
60+
-Verbose
61+
}
62+
displayName: 'Copying test app zip artifacts to blob'

azure-pipelines-2.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Starter pipeline
2+
# Start with a minimal pipeline that you can customize to build and deploy your code.
3+
# Add steps that build, run tests, deploy, and more:
4+
# https://aka.ms/yaml
5+
6+
#trigger:
7+
#- master
8+
#- dev
9+
10+
pool:
11+
vmImage: 'vs2017-win2016'
12+
13+
variables:
14+
Configuration: Release
15+
16+
steps:
17+
- pwsh: |
18+
$ErrorActionPreference = "Stop"
19+
./test/E2E/Start-E2ENightlyTest.ps1
20+
env:
21+
AzureWebJobsStorage: $(AzureWebJobsStorage)
22+
AzureWebJobsCosmosDBConnectionString: $(AzureWebJobsCosmosDBConnectionString)
23+
AzureWebJobsServiceBus: $(AzureWebJobsServiceBus)
24+
AzureWebJobsEventHubSender: $(AzureWebJobsEventHubSender)
25+
FUNCTIONS_WORKER_RUNTIME : "powershell"
26+
FunctionAppUrl: $(FunctionAppUrl)
27+
continueOnError: true
28+
displayName: 'Running E2ETest'
29+
30+
- task: CopyFiles@2
31+
inputs:
32+
SourceFolder: '$(System.DefaultWorkingDirectory)/testResults'
33+
Contents: '*.trx'
34+
TargetFolder: '$(Build.ArtifactStagingDirectory)'
35+
displayName: 'Copying test result file for artifacts'
36+
37+
- task: PublishBuildArtifacts@1
38+
inputs:
39+
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
40+
ArtifactName: 'drop'
41+
publishLocation: 'Container'
42+
displayName: 'Publishing build and test result artifacts'

azure-pipelines.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Starter pipeline
2+
# Start with a minimal pipeline that you can customize to build and deploy your code.
3+
# Add steps that build, run tests, deploy, and more:
4+
# https://aka.ms/yaml
5+
6+
#trigger:
7+
#- master
8+
#- dev
9+
10+
pool:
11+
vmImage: 'vs2017-win2016'
12+
13+
variables:
14+
Configuration: Release
15+
16+
steps:
17+
- pwsh: ./build.ps1 -NoBuild -Bootstrap
18+
displayName: 'Running ./build.ps1 -NoBuild -Bootstrap'
19+
20+
- pwsh: |
21+
$ErrorActionPreference = "Stop"
22+
./build.ps1 -Clean -Configuration Release
23+
displayName: './build.ps1 -Clean -Configuration Release'
24+
25+
- pwsh: ./build.ps1 -NoBuild -Test
26+
displayName: 'Running UnitTest'
27+
28+
- pwsh: ./test/E2E/Start-E2ETest.ps1
29+
env:
30+
AzureWebJobsStorage: $(AzureWebJobsStorage)
31+
AzureWebJobsCosmosDBConnectionString: $(AzureWebJobsCosmosDBConnectionString)
32+
AzureWebJobsServiceBus: $(AzureWebJobsServiceBus)
33+
AzureWebJobsEventHubSender: $(AzureWebJobsEventHubSender)
34+
FUNCTIONS_WORKER_RUNTIME : "powershell"
35+
displayName: 'Running E2ETest'
36+
37+
- task: CopyFiles@2
38+
inputs:
39+
SourceFolder: '$(System.DefaultWorkingDirectory)/testResults'
40+
Contents: '*.trx'
41+
TargetFolder: '$(Build.ArtifactStagingDirectory)'
42+
displayName: 'Copying test result file for artifacts'
43+
44+
- task: PublishBuildArtifacts@1
45+
inputs:
46+
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
47+
ArtifactName: 'drop'
48+
publishLocation: 'Container'
49+
displayName: 'Publishing build and test result artifacts'

docs/designs/PowerShell-AzF-Overall-Design.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [Host Request Processing](#host-request-processing)
99
- [Intuitive User Experience](#intuitive-user-experience)
1010
- [Concurrent Invocation of Functions](#concurrent-invocation-of-functions)
11+
- [Managed Dependencies](#managed-dependencies)
1112
- [Durable Functions Prototype](#durable-functions-prototype)
1213

1314
## Introduction
@@ -459,6 +460,56 @@ Once a PowerShell Manager instance is checked out, the worker's main thread will
459460

460461
Note that, checking out a PowerShell Manager instance from the pool is a blocking operation. When the number of concurrent invocations reaches the pool size, the worker's main thread will block on the checkout operation for the next invocation request until a PowerShell Manager instance becomes available. The initial design was to let the checkout operation happen in the thread-pool thread too, so the main thread would just create a task and go back processing the next request. However, it turned out to result in high lock contention due to too many tasks competing during the checkout operation. Therefore, the design was changed to make the checkout operation happen only in main thread, and a task gets created only after a PowerShell Manager instance becomes available.
461462

463+
## Managed Dependencies
464+
465+
### Problem
466+
467+
The goal is to let the user declare the dependencies required by functions, and rely on the service automatically locating and installing the dependencies from the PowerShell Gallery or other sources, taking care of selecting the proper versions, and automatically upgrading the dependencies to the latest versions (if allowed by the version specifications provided by the user).
468+
469+
Dependencies are declared in the _requirements.psd1_ file (_manifest_) as a collection of pairs (<_name_>, <_version specification_>). Currently, the version specification should either be an exact and complete version, or strictly match the following pattern: `<major version>.*`. So, a typical manifest may look like this:
470+
471+
``` PowerShell
472+
@{
473+
'Az' = '2.*'
474+
'PSDepend' = '0.*'
475+
'Pester' = '5.0.0-alpha3'
476+
}
477+
```
478+
479+
When the `<major version>.*` format is used, the worker will retrieve the latest available module version (within the specified major version) from the PowerShell Gallery, ignoring prerelease versions.
480+
481+
When the exact version is specified, the worker will retrieve the specified version only, ignoring any other version. Prerelease versions are allowed in this case.
482+
483+
The number of entries in the _requirements.psd1_ file should not exceed **10**. This limit is not user-configurable.
484+
485+
Installing and upgrading dependencies should be performed automatically, without requiring any interaction with the user, and without interfering with the currently running functions. This represents an important design challenge. In a different context, dependencies could be stored on a single location on the file system, managed by regular PowerShell tools (`Install-Module`/`Save-Module`, `PSDepend`, etc.), while having the same file system location added to _PSModulePath_ to make all the modules available to scripts running on this machine. This is what PowerShell users normally do, and this approach looks attractive because it is simple and conventional. However, in the contexts where multiple independent workers load modules and execute scripts concurrently, and at the same time some module versions are being added, upgraded, or removed, this simple approach causes many known problems. The root causes of these problems are in the fundamentals of PowerShell and PowerShell modules design. The managed dependencies design in Azure Functions must take this into account. The problems will be solved if we satisfy the following conditions:
486+
487+
- **Only one writer at a time**. No concurrent executions of `Save-Module`, `PSDepend`, or anything else that could perform changes _on the same target file path_*_.
488+
- **Atomic updates**. All workers executing a PowerShell script should always observe a state of dependency files that is a result of a _successful and complete_ execution of `Save-Module` or a similar tool. The workers should never observe any partial results.
489+
- **Immutable view**. As soon as a set of dependency files is exposed to a worker for loading module purposes for the first time, this set of files should never change _during the life time of this worker._ Deletions, additions, or modifications are not acceptable.
490+
491+
### Solution
492+
493+
The main design idea is to partition the installed dependencies in such a way that every PowerShell worker gets exactly one complete and immutable set of dependencies for the lifetime of this worker (until restart). The same set of dependencies can be shared by multiple PowerShell workers, but each worker is strictly tied to a single set.
494+
495+
On the first function load request, the PowerShell worker reads the manifest and installs all the required dependencies into a dedicated folder on a file storage shared between all PowerShell workers within the function app. The subsequent function invocation requests are blocked until the installation of _all_ the dependencies is _completed successfully_. After the successful installation, the PowerShell worker insert the path of the folder to the '_PSModulePath_' variable of the PowerShell worker process, so that they become available to all functions running on this worker, and allows function invocation requests to proceed. The path to this folder is inserted into '_PSModulePath_' _before_ the `functionAppRoot\Modules` path, so that the managed dependencies folder is discovered first when modules are imported. This entire set of dependencies becomes an immutable _snapshot_: once created and exposed to PowerShell workers, it never changes: no modules or module versions are ever added to this snapshot or removed from this snapshot.
496+
497+
On the next function load request (normally received on a worker start), the worker reads the manifest and checks if the latest snapshot contains the dependencies satisfying the manifest. If the latest snapshot is _acceptable_, the worker makes '_PSModulePath_' point to this snapshot folder and allows the next function invocation proceed immediately. At the same time, the worker starts a background installation of all the dependencies into a new folder, which after successful completion becomes the latest snapshot. At this point, other starting workers will be able to find and use the new snapshot. The workers that were already running when the new snapshot was installed will be able to use it after restart.
498+
499+
A snapshot is considered _acceptable_ if it contains any version _allowed_ by the manifest for each required dependency. For example, Az 2.1 is allowed by the manifest containing `'Az' = '2.*'`, so the snapshot containing Az 2.1 will be considered acceptable, even if Az 2.2 is published on the PowerShell Gallery. As a result, the next function invocation will be allowed to proceed with Az 2.1 without waiting for any other Az version to be installed, and without even trying to contact the PowerShell Gallery for discovering the latest module version. All these activities can be performed in the background, without blocking function invocations.
500+
501+
However, if the latest snapshot is _not acceptable_ (i.e. it does not contain module versions required by the manifest), the worker starts installing the dependencies into a new snapshot, and all the subsequent function invocation requests are blocked, waiting for the new snapshot installation to complete.
502+
503+
When a snapshot installation starts, the dependencies are first installed into a folder with a name following a special pattern (`*i`), so that this snapshot is not picked up by any worker prematurely, before the installation is complete. After _successful_ completion, the snapshot is _atomically promoted_ by renaming the folder to follow a different pattern (`*r`), which indicates to other workers that this snapshot is ready to use. If the installation fails or cannot complete for any reason (for example, the worker restarts, crashes, or gets decommissioned), the folder stays in the installing state until removed.
504+
505+
Incomplete and old snapshots that are no longer in use are periodically removed from the file storage. In order to allow detecting unused snapshots, each PowerShell worker keeps "touching" a file named `.used` at the root of the used snapshot folder every `MDHeartbeatPeriod` minutes. Before and after installing any new snapshot, every PowerShell worker looks for unused snapshots by checking the folder creation time and the `.used` file modification time. If both these time values are older than (`MDHeartbeatPeriod` + `MDOldSnapshotHeartbeatMargin`) minutes, the snapshot is considered unused, so the PowerShell worker removes it. The latest `MDMinNumberOfSnapshotsToKeep` snapshots will never be removed, regardless of usage.
506+
507+
`MDHeartbeatPeriod`, `MDOldSnapshotHeartbeatMargin`, and `MDMinNumberOfSnapshotsToKeep` are environment variables configurable via Application Settings of a Function App. `MDHeartbeatPeriod` and `MDOldSnapshotHeartbeatMargin` are expected to contain strings in the format that can be parsed by `System.TimeSpan.Parse` method.
508+
509+
No other changes are ever performed to the snapshots that were once installed.
510+
511+
In this design, upgrading dependencies is conceptually decoupled from executing functions. Upgrading dependencies can be performed on any schedule by one or multiple agents, (almost) without any coordination between each other or with the workers executing functions. This allows us to make independent decisions on whether to run it from a separate service or keep it in PowerShell workers, and schedule the upgrade as often as we want. For now, upgrading dependencies is still performed by the PowerShell workers, just to avoid the overhead of deploying and maintaining a separate service. However, the design keeps this option open.
512+
462513
## Durable Functions Prototype
463514

464515
The [Durable Functions](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-concepts) is essentially the stateful workflow in a serverless environment. It consists of an orchestrator function and one or more activity functions. Durable Functions runs in a completely asynchronous way: the orchestrator function will stop after triggering an activity function, and later gets restarted once the activity function finishes execution. The actions of triggering activity functions and the activity function results will be saved along the way. After the orchestrator function is restarted, it replays the execution from the very beginning, but will skip the actions that are already done and use the results directly from the saved logs.

src/AssemblyInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
using System.Runtime.CompilerServices;
77

88
[assembly:InternalsVisibleTo("Microsoft.Azure.Functions.PowerShellWorker.Test")]
9-
9+
[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2")]

src/DependencyManagement/DependencyInfo.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement
88
internal class DependencyInfo
99
{
1010
internal readonly string Name;
11-
internal readonly string MajorVersion;
12-
internal readonly string LatestVersion;
11+
internal readonly string ExactVersion;
1312

14-
internal DependencyInfo(string name, string majorVersion, string latestVersion)
13+
internal DependencyInfo(string name, string exactVersion)
1514
{
1615
Name = name;
17-
MajorVersion = majorVersion;
18-
LatestVersion = latestVersion;
16+
ExactVersion = exactVersion;
1917
}
2018
}
2119
}

src/DependencyManagement/DependencyManagementUtils.cs

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)