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

Image component in .NET SDK #115

Closed
wants to merge 11 commits into from

Conversation

mikhailshilkov
Copy link
Member

Initial support for .NET SDK, mainly focused on the Image component

@mikhailshilkov
Copy link
Member Author

I added a couple of examples in pulumi/examples#482 to test this change

@mikhailshilkov
Copy link
Member Author

I'm still not a collaborator on this repo. Admins - please set up the NuGet publishing keys.

@CyrusNajmabadi I would appreciate your review, especially in parts of using inputs/outputs - maybe we can simplify them or add helpers to the base library.

@CyrusNajmabadi
Copy link
Contributor

@pgavlin can you setup collaborator access for @mikhailshilkov ?

//
// Pick a reasonably distributed number between 0 and 2^30. This will fit as an int32
// which the grpc layer needs.
var streamID = (int)Math.Floor(new Random().NextDouble() * (1 << 30));
Copy link
Contributor

Choose a reason for hiding this comment

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

put new Random() in a static, and lock around it. i don't believe Random is threadsafe, and it shouldn't be recreated each time.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, streamID is not needed in current .NET implementation

Copy link
Member Author

Choose a reason for hiding this comment

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

Re-introduced streamId and a static random generator for async stream of outputs

process.StartInfo.RedirectStandardInput = stdin != null;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();
Copy link
Contributor

Choose a reason for hiding this comment

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

we might want to consider a try/catch around this code. .net process stuff can be finicky in my experience

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 try/catch around the process call, returning (1, ex.ToString()) in case an exception is thrown. Is that what you intended?

@lukehoban lukehoban mentioned this pull request Nov 27, 2019
if (stdin != null)
{
process.StandardInput.Write(stdin);
process.StandardInput.Close();
Copy link
Contributor

Choose a reason for hiding this comment

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

interesting. we need to close this? why?

Copy link
Member Author

Choose a reason for hiding this comment

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

otherwise the process might keep waiting for more input. I think there was a similar close in the node sdk

// this point because Docker uses stderr for both errors and warnings. So, instead, we
// just collect the messages, and wait for the process to end to decide how to report
// them.
var stderr = await process.StandardError.ReadToEndAsync().ConfigureAwait(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

i'm always terrified of stdout/err in .net. is there any chance the async calls could hang because the first one is awaiting completion, but the process isn't completing because it's trying to write to the other, and nothing is reading from it?

In other words, should we kick off two Task.Runs to ensure that the readings will happen concurrently so we don't get any deadlocks?

Copy link
Member Author

Choose a reason for hiding this comment

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

that's a valid point... I guess we should do whatever we can to prevent deadlocks

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 realized that the node SDK was broadcasting output lines as they come, while my .NET implementation was waiting for the complete output. Now, I changed to async output consumption based on OutputDataReceived event. This should also address the potential deadlock.

Copy link
Contributor

Choose a reason for hiding this comment

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

should we not do this for streams? i.e. why do you still ReadToEndAsync with .StandardError?

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed. Are we good to go?

public static Input<T?> Unwrap<T>(this Input<T>? input) where T : class
{
return input == null ? Output.Create((T?)null) : input.Apply(v => (T?)v);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

these seems like useful enough to move to the Pulumi layer. can you file issue/PR about that?

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 split the class to Extensions (generic) and DockerExtension (specific). There are a log of potential generic API based on combinations of inputs, lists, maps, unions, nullable/non-nullable, class/struct, plain/output/input, etc. So, I'm torn on the value of my extensions: are they useful enough to warrant the complexity? What are good names for them?
Looking forward to your opinion.

.Apply(v => v != null ? new CacheFromUnwrap { Stages = v } : null);
}

public static Input<Union<string, DockerBuildUnwrap>> Unwrap(this InputUnion<string, DockerBuild> build)
Copy link
Contributor

Choose a reason for hiding this comment

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

these could be represented with a lower layer extension that operates on an InputUnion<X, Y> and which takes some helper functions to map from an X to an UnwrapX.

});
}

public static Input<ImageRegistryUnwrap?> Unwrap(this Input<ImageRegistry>? registry)
Copy link
Contributor

Choose a reason for hiding this comment

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

would love to move this down as per the above discussion.

arguments.Append(EscapeArguments((string)argEnumerator.Current!));
}

return arguments.ToString();
Copy link
Contributor

Choose a reason for hiding this comment

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

this entire method looks like it's simply: string.Join(" ", args.Select(EscapeArguments))

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed

/// </summary>
private static string EscapeArguments(string argument)
{
using var characterEnumerator = argument.GetEnumerator();
Copy link
Contributor

Choose a reason for hiding this comment

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

i'm not getting the need for manual enumeratoin. You only seem to process a single char at a time. so just do a foreach (var c in argument) ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed

}

// Append the final "
escapedArgument.Append('\"');
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this duplicates your switch case above. consider a helper.

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you mean here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Gotcha!

Copy link
Member Author

Choose a reason for hiding this comment

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

These lines are a bit different though. I refactored the loops to one-liners.

internal static class UnwrapExtensions
{
public static Input<ImmutableArray<T>?> Unwrap<T>(this InputList<T>? items)
=> items == null ? Output.Create((ImmutableArray<T>?)null) : items.Apply(v => (ImmutableArray<T>?)v);
Copy link
Contributor

Choose a reason for hiding this comment

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

feels like this should be returning an Output<ImmutableArray<T>?> instead of an Input<ImmutableArray<T>?>.

(Same comment for most of these).

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 restructured all extensions one more time. The common ones are all expressed in terms of ToOutput, Apply, and MapT0/1. The specific ones are Unwrap that rely on common ones.

The only problem is that I need to preserve null-iness: if an input is nullable, I want the output to contain a nullable, and vice versa. It seems that it's not possible to make nullable and non-nullable overloads of otherwise equal methods, so I had to name them ToOutputNullable etc. Not a fan, but I don't see other options yet.

@mikhailshilkov mikhailshilkov added this to the 0.30 milestone Dec 2, 2019
=> union.Apply(v => v.IsT0 ? func(v.AsT0).Apply(v => Union<U, T1>.FromT0(v)) : Output.Create(Union<U, T1>.FromT1(v.AsT1)));

public static Output<Union<T0, U>> ApplyT1<T0, T1, U>(this Output<Union<T0, T1>> union, Func<T1, Output<U>> func)
=> union.Apply(v => v.IsT1 ? func(v.AsT1).Apply(v => Union<T0, U>.FromT1(v)) : Output.Create(Union<T0, U>.FromT0(v.AsT0)));
Copy link
Contributor

Choose a reason for hiding this comment

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

will you move tehse down later?

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 can move them to Pulumi if we agree on the API.

Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

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

Overall this LGTM, just have a couple of small concern about process management i'd like clarified.

@CyrusNajmabadi
Copy link
Contributor

Code LGTM. Can we have a single test just to smoke test this?

@mikhailshilkov
Copy link
Member Author

Closing in favor of #119 which can run the tests

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