Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to module approach #20

Merged
merged 9 commits into from
Aug 31, 2018
Merged

Switch to module approach #20

merged 9 commits into from
Aug 31, 2018

Conversation

TylerLeonhardt
Copy link
Member

This PR features:

  • Everything gets run in local scope
  • the module is exposed to the user - they can do Push-OutputBinding and Get-OutputBinding
  • some refactoring of the PowerShellManager class to make composing powershell easier

I learned that .AddStatement() exists which basically does what a ; does in a script.

// Reset the runspace to the Initial Session State
_pwsh.Runspace.ResetRunspaceState();

// TODO: Change this to clearing the variable by running in the module
Copy link
Member Author

Choose a reason for hiding this comment

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

@daxian-dbw I remember you mentioning there's a way to do this but I don't remember. Can you enlighten me 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's OK to just call ResetRunspaceState (reset the variable table) for now, and wait for feedback.
If it's necessary, we can go wild on refreshing the Runspace:

  • we can sweep the global function table to remove the non-built-in free functions (not from module)
  • we can remove all modules after a function run
  • we can analyze the function script to explicitly disallow using global: for variable/function declaration, and -scope global for *-Variable.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh I meant refreshing the module scope variable as in setting $script:OutputBindings = @{}

Copy link
Member Author

Choose a reason for hiding this comment

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

Right now I'm just force importing the module.

Copy link
Contributor

@daxian-dbw daxian-dbw Aug 30, 2018

Choose a reason for hiding this comment

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

Oh, easy, add a parameter -Purge to Get-OutputBinding. When specified, it clears the internal hashtable before returning.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think we should expose a -Purge to the user. We talked about this yesterday.

Something like
& $module { $script:OutputBindings = @{}}

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't remember the exact syntax you brought up

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, we talked about the Clean function. I don't really have a strong opinion on the Clean function, but it might be overkill to have a separate function just for that simple operation.

We don't give the user the ability to grab the instance of the internal hashtable, but I think it's OK for them to purge it if they want. For the language worker, if we are supposed to receive something but Get-OutputBinding returns nothing, then it's an error to report to the Host.

Copy link
Contributor

@daxian-dbw daxian-dbw Aug 30, 2018

Choose a reason for hiding this comment

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

I thought about holding a reference to the hashtable in C#. If we want to go that road, then we can make the module accept argument -- a Hashtable instance. If the argument is presetnt, then the module uses that as the internal container; if not present, then create a new one.

But I do think in the local testing scenario, user will want to purge the output key/value pairs sometimes.

.AddParameter("Name", modulePath)
.AddParameter("Scope", "Global")
.AddParameter("Force")
.InvokeAndClearCommands();
Copy link
Contributor

Choose a reason for hiding this comment

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

We can choose to remove all modules but this one, because we know this will be needed for the next function run.

But for now, I think we can skip removing any modules given the fact that 1 function app maps to N language worker and 1 language worker maps to 1 function app. Let's wait for the feedback.

@@ -60,63 +41,35 @@ internal class PowerShellManager
}

public static PowerShellManager Create(RpcLogger logger)
Copy link
Contributor

Choose a reason for hiding this comment

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

Better to avoid this factory method and use the constructor directly. This SO question has a pretty good discussion about what a factory method is mainly for.

Quoted from https://blogs.msdn.microsoft.com/kcwalina/2004/10/11/design-guidelines-update-factories-vs-constructors/:

Consider using a constructor instead of a Factory. Only after this thought process should you proceed with the implementation of a Factory.

script.Append("' = $");
}
script.AppendLine(binding.Key);
initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't know we can set the execution policy this way, great!
We can skip the platform check and always set to Unrestricted for all platforms (even though it execution policy doesn't work on Unix).

string modulePath = System.IO.Path.Join(
AppDomain.CurrentDomain.BaseDirectory,
"Azure.Functions.PowerShell.Worker.Module",
"Azure.Functions.PowerShell.Worker.Module.psd1");
Copy link
Contributor

Choose a reason for hiding this comment

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

In my PR, I updated the .csproj file to bin-place the module in the folder Modules that sits next to the worker assembly. We should add that folder to the module path at the initialization phase.

.AddCommand("Import-Module")
.AddParameter("Name", modulePath)
.AddParameter("Scope", "Global")
.InvokeAndClearCommands();
Copy link
Contributor

Choose a reason for hiding this comment

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

The rest of the initialization should be done when the WorkerInitRequest comes in. Once we add the Modules folder in the module path, we don't need to explicitly import this module.

_pwsh
.AddScript($@". {scriptPath}", s_UseLocalScope)
.AddStatement()
.AddCommand(entryPoint, s_UseLocalScope);
Copy link
Contributor

Choose a reason for hiding this comment

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

You are running the script twice, which could potentially cause problems, such as some operations get to done twice and fails in the second attempt for other side effects.

When the user specify the entrypoint, we treat the script as a module, and run import-module on it (useLocalScope won't be needed). So the script as a whole only gets run once. After the function run, we remove the module in the cleanup phase.

readonly static string s_TriggerMetadataParameterName = "TriggerMetadata";
readonly static bool s_UseLocalScope = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. They should be const if they are not changed.
  2. The explicit modifier is always preferred.
  3. s_UseLocalScope is not very useful, better replace it with useLocalScope: true in the method call.

Collection<PSObject> pipelineItems = _pwsh.InvokeAndClearCommands<PSObject>();
foreach (var psobject in pipelineItems)
{
_logger.LogInformation(psobject.ToString());
Copy link
Contributor

Choose a reason for hiding this comment

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

The log message needs to be reworked. It should call out that this is what returned from the function run.

Copy link
Member Author

Choose a reason for hiding this comment

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

prepend it with FROM FUNCTION: ?

Copy link
Contributor

Choose a reason for hiding this comment

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

How about OUTPUT: ?

if(returnObject != null)
{
result.Add("$return", returnObject);
}
ResetRunspace();
return result;
}
catch(Exception e)
{
ResetRunspace();
Copy link
Contributor

Choose a reason for hiding this comment

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

ResetRunspace(); should be put in the finally block.

@@ -67,6 +69,7 @@ internal static class HandleInvocationRequest
// Set out binding data and return response to be sent back to host
foreach (KeyValuePair<string, BindingInfo> binding in functionInfo.OutputBindings)
{
// TODO: How do we want to handle when a binding is not set?
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, we do. That is an error, we should return fail statue in the InvocationResponse message.

@daxian-dbw
Copy link
Contributor

BTW, when using any built-in cmdlet, please use the fully qualified format, such as Microsoft.PowerShell.Core\Get-Command. This is the best practice followed by PowerShellGet and other script modules.

@TylerLeonhardt TylerLeonhardt force-pushed the switch-to-module-approach branch from 15f145b to 6c0c016 Compare August 30, 2018 22:11
// Prepend the path to the internal Modules folder to the PSModulePath
var modulePath = Environment.GetEnvironmentVariable("PSModulePath");
var additionalPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules");
Environment.SetEnvironmentVariable("PSModulePath", $"{additionalPath}{Path.PathSeparator}{modulePath}");
Copy link
Member Author

Choose a reason for hiding this comment

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

@daxian-dbw I could add one for the user as well:

myFuncApp/Modules:workerCode/Modules:normalPSModulePath

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, adding myFunApp/Modules makes sense.

src/PowerShell/PowerShellManager.cs Outdated Show resolved Hide resolved
src/PowerShell/PowerShellManager.cs Outdated Show resolved Hide resolved
parameterMetadata = _pwsh
.AddScript($@". {scriptPath}")
.AddStatement()
.AddCommand("Get-Command", useLocalScope: true).AddParameter("Name", entryPoint)
Copy link
Contributor

Choose a reason for hiding this comment

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

When running c# cmdlet, there is no need to run it in a local scope, as c# cmdlet doesn't create scope anyway.

_pwsh.AddScript($@". {scriptPath} @args");
parameterMetadata = _pwsh.AddCommand("Get-Command", useLocalScope: true).AddParameter("Name", scriptPath)
.InvokeAndClearCommands<ExternalScriptInfo>()[0].Parameters;
_pwsh.AddCommand(scriptPath, useLocalScope: true);
Copy link
Contributor

Choose a reason for hiding this comment

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

If you are not dot-sourcing the script, it will run in its script scope, which is a local scope, so no need to specify useLocalScope: true.

returnObject = pipelineItems[pipelineItems.Count - 1];
}

var result = _pwsh.AddCommand("Azure.Functions.PowerShell.Worker.Module\\Get-OutputBinding", useLocalScope: true)
Copy link
Contributor

Choose a reason for hiding this comment

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

The cmdlet will run in local scope by default when not dot-sourcing, so useLocalScope: true is not needed.

Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process
}
";

readonly static string s_TriggerMetadataParameterName = "TriggerMetadata";
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be const.

Collection<PSObject> pipelineItems = _pwsh.InvokeAndClearCommands<PSObject>();
foreach (var psobject in pipelineItems)
{
_logger.LogInformation($"FROM FUNCTION: {psobject.ToString()}");
Copy link
Contributor

Choose a reason for hiding this comment

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

How about OUTPUT: as the prefix?

parameterMetadata = ExecuteScriptAndClearCommands<FunctionInfo>($@"Get-Command {entryPoint}")[0].Parameters;
_pwsh.AddScript($@". {entryPoint} @args");
parameterMetadata = _pwsh
.AddCommand("Import-Module")
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use fully qualified name, same to other places like Get-Command.

.AddCommand("Get-Command", useLocalScope: true).AddParameter("Name", entryPoint)
.InvokeAndClearCommands<FunctionInfo>()[0].Parameters;

_pwsh.AddCommand(entryPoint, useLocalScope: true);
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to use useLocalScope: true when you are not dot-sourcing. Please check all occurrences.

_pwsh.AddScript($@". {entryPoint} @args");
parameterMetadata = _pwsh
.AddCommand("Import-Module")
.AddParameter("Name", scriptPath)
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be moved to the above line, as this parameter and the command are logically grouped.

// If the function had an entry point, this will remove the module that was loaded
var moduleName = Path.GetFileNameWithoutExtension(scriptPath);
_pwsh.AddCommand("Get-Module").AddParameter("Name", moduleName)
.AddCommand("Remove-Module").InvokeAndClearCommands();
Copy link
Contributor

Choose a reason for hiding this comment

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

A Remove-Module -ErrorAction SilentlyContinue would do. A bit faster than having a pipeline.

// This script handles when the user adds something to the pipeline.
// It logs the item that comes and stores it as the $return out binding.
// The last item stored as $return will be returned to the function host.
private const string s_TriggerMetadataParameterName = "TriggerMetadata";
Copy link
Contributor

Choose a reason for hiding this comment

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

The field name should be changed to TriggerMetadataParameterName, since it's const.

Copy link
Member Author

Choose a reason for hiding this comment

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

added a _ since it's private

@TylerLeonhardt
Copy link
Member Author

I've run all the tests ran the example

Copy link
Contributor

@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

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

LGTM

@TylerLeonhardt TylerLeonhardt merged commit 6583e49 into dev Aug 31, 2018
@TylerLeonhardt TylerLeonhardt deleted the switch-to-module-approach branch August 31, 2018 00:09
davidmrdavid added a commit that referenced this pull request Apr 7, 2022
# This is the 1st commit message:

separate DF SDK classes from DF worker classes

# This is the commit message #2:

fix typo

# This is the commit message #3:

DurableSDK now compiles by itself

# This is the commit message #4:

Allow ExternalSDK to handle orchestration

# This is the commit message #5:

document next steps

# This is the commit message #6:

allow external SDK to set the user-code's input. Still need to refactor this logic for the worker to continue working with old SDK

# This is the commit message #7:

add import module

# This is the commit message #8:

supress traces

# This is the commit message #9:

avoid nullptr

# This is the commit message #10:

pass tests

# This is the commit message #11:

fix E2E tests

# This is the commit message #12:

develop E2E tests

# This is the commit message #13:

Enabled external durable client (#765)

Co-authored-by: Michael Peng <[email protected]>
# This is the commit message #14:

bindings work

# This is the commit message #15:

conditional binding intialization

# This is the commit message #16:

conditional import

# This is the commit message #17:

Added exception handling logic

# This is the commit message #18:

Revert durableController name to durableFunctionsUtils

# This is the commit message #19:

Ensure unit tests are functioning properly

# This is the commit message #20:

Corrected unit test names

# This is the commit message #21:

Turned repeated variables in unit tests into static members

# This is the commit message #22:

Fixed issue with building the worker

# This is the commit message #23:

Fix E2E test

# This is the commit message #24:

Fixed unit test setup

# This is the commit message #25:

Fixed another unit test setup

# This is the commit message #26:

Remove string representation of booleans

# This is the commit message #27:

patch e2e test
# This is the commit message #28:

remove typo in toString
# This is the commit message #29:

Update PowerShell language worker pipelines (#750)

* Install .Net to a global location

* Remove .Net installation tasks

* Update install .Net 6 task

* Update Windows image to use windows-latest
# This is the commit message #30:

Make throughput warning message visible for tooling diagnosis (#757)


# This is the commit message #31:

Update grpc.tools to version 2.43.0

# This is the commit message #32:

Update Google.Protobuf.Tools to version 3.19.4

# This is the commit message #33:

Revert "Update Google.Protobuf.Tools to version 3.19.4"

This reverts commit bcbd022.

# This is the commit message #34:

Revert "Update grpc.tools to version 2.43.0"

This reverts commit ccb323a.

# This is the commit message #35:

Update Google.Protobuf to 3.19.4 and grpc.tools to  2.43.0 (#762)

* Update grpc.tools to version 2.43.0

* Update Google.Protobuf.Tools to version 3.19.4
# This is the commit message #36:

Switch from Grpc.Core to Grpc.Net.Client (#758)

* Upgraded protobuf versions and removed Grpc.Core dependency

* Updated channel and option types used

* Change channel credentials

* Added http prefix to url

* Add valid URL check and explicitly include credentials
# This is the commit message #37:

Update pipeline logic to generate the SBOM for release builds (#767)
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.

2 participants