Skip to content
This repository has been archived by the owner on Mar 16, 2022. It is now read-only.

feature/go-support #103

Closed
wants to merge 42 commits into from
Closed

feature/go-support #103

wants to merge 42 commits into from

Conversation

marcellanz
Copy link
Contributor

This PR implements Go support as a CloudState API Client Library and should fix #3 to a good amount.

Included is

  • The implementation of the Discovery and EventSourced Services of CloudState.
  • The implementation of the TCK Shopping Cart example, used to be verified by the TCK.
  • build support within travis-ci so that the TCK can validate the shopping-cart example (passes by 100%).
  • a built.sbt task to build the Go binary with a Go Docker Container

…entation. Passes the TCK 100%. PoC included for reference; will be removed.
…. removed redundant ServiceInfo initialization.
…ith a docker container for *NIX and Windows (prepared).
…tation, snapshot interfaces added, minor cleanups (err flows,...)
…ting method if an event is not handled by the implemented EventHandler#HandleEvent
@viktorklang
Copy link
Contributor

Thanks @marcellanz for opening this exciting PR!

I'm definitely no Go-expert so it's going to take some time to digest :)
Is it possible to avoid commiting the generated .pb.go-files?
Is it possible to avoid committing the .proto files for the protocol again, and instead refer to the files in protocols/?

Looking forward to having a look at this! :)

@marcellanz
Copy link
Contributor Author

marcellanz commented Sep 16, 2019

@viktorklang
:-) ahh... I opened the PR as a "Draft" to prepare it and its visible now :-o
But I'll fix some things your asked already (see below)

Thanks @marcellanz for opening this exciting PR!
I'm definitely no Go-expert so it's going to take some time to digest :)

"Go is simple and you should be able to learn it in a very short amount of time"(tm)
I'm curious about how well it is to read/understand for you, especially as you know the domain very well. As Go is a very simple (boring) language, it should be easy to follow. I hope its understandable. I tried my best to be explicit and idiomatic as possible as Go is usually explicit and has no magic going on.

The most magic stuff happens with reflection; by which we need a bit of because of the dynamic nature of handlers. There is also one or two of sub-optimal things about the gRPC implementation; a ServiceDescriptor is not exposed explicitly by it and so it makes things like the registration of the entity and its service cumbersome.I iterated quite some times over that and hope I found something as expressive as possible without too much implicit magic.

Actually the current form is explicit as a user can pass the proto filename; but also one of the proto message types used by the service which is typesafe, as I prefer it, but not to be expected => why "register" one of the services messages types go get a gRPC service registered.
I saw, the Java gRPC implementation is more welcoming in this sense and there are open issues on Go's gRPC project to have descriptors accessible (golang/protobuf#364, golang/protobuf#293, golang/protobuf#489) but it is like it is for now and I did not want to introduce a complex implementation of it based on heavy internal knowledge of gRPCs implementation.

Is it possible to avoid commiting the generated .pb.go-files?
Is it possible to avoid committing the .proto files for the protocol again, and instead refer to the files in protocols/?

I have both on an issue list on my branch here of what next steps for go-support might be here
https://github.com/marcellanz/cloudstate/issues/2

I'll clean up the proto file handling and its generation to be ready for this PR.

Looking forward to having a look at this! :)

Thanks, I'm looking forward to your feedback.

@viktorklang
Copy link
Contributor

@marcellanz I took a stab at some of the build integration and proto generation things here: 5b9df7c

My Go is definitely not fluent enough :D I'll start looking at the Go-specific code shortly :)

I assume(?) that we want to split the go-support up into a library and the shopping-cart example, and put the go-shopping-cart in samples/… ?

I'm still not sure about the best way of running the compilation, if we should assume Go is installed or not, or run it in Docker.

@marcellanz
Copy link
Contributor Author

marcellanz commented Sep 16, 2019

@marcellanz I took a stab at some of the build integration and proto generation things here: 5b9df7c

nice, thank you!

My Go is definitely not fluent enough :D I'll start looking at the Go-specific code shortly :)

I assume(?) that we want to split the go-support up into a library and the shopping-cart example, and put the go-shopping-cart in samples/… ?

Yes, I'd support that and is what I had in my mind also to go forward. The *.proto files would be the only artefacts to share between libraries I think. "go imports" also expect that a go package lives on the edge of a VCS repository

I'm still not sure about the best way of running the compilation, if we should assume Go is installed or not, or run it in Docker.

That was my motivation to introduce the Docker supported go compilation; so that the project as a whole can run the TCK and a CloudState developer doesn't need all compilers (Go, "node", Swift,...) installed correctly, which might be tricky, for the project. IMO the docker approach is kind of elegant to liberate the developers environment from that burden.

Having a library of a language support implementation would mean a separate repository, yes? It would be the most effective way to go for a Go library as Go's module and package management expects to use VCS endpoints for packages and imports directly.
I don't know too much about how a TCK should be structured, but it could even be outside of the main cloudstate repository and be together with something that builds "cloudstate + (any user function language support)" is needed. The CloudState "reference implementation" would be always a JVM based one; so keep these implementations together would be natural for a JVM based reference implementation of CloudState.

I have also not thought too much about the topic, but it came to me as I would have to provide a canonical "godoc" story which assumes to have a separate repository for the go code.

@viktorklang
Copy link
Contributor

@marcellanz

Yes, I'd support that and is what I had in my mind also to go forward. The *.proto files would be the only artefacts to share between libraries I think. "go imports" also expect that a go package lives on the edge of a VCS repository

I'd be totally open to have a cloudstateio/go-support repository, perhaps we should consider this for all language supports, at least when the CloudState Specification is more formalized, as there is less maintenance burden due to making sweeping changes across the board.

That was my motivation to introduce the Docker supported go compilation; so that the project as a whole can run the TCK and a CloudState developer doesn't need all compilers (Go, "node", Swift,...) installed correctly, which might be tricky, for the project. IMO the docker approach is kind of elegant to liberate the developers environment from that burden.

Indeed. I'm not sure how we'll deal with Linux/Mac/Win support for that though. We'll need to figure out the testing matrix at some point.

Having a library of a language support implementation would mean a separate repository, yes? It would be the most effective way to go for a Go library as Go's module and package management expects to use VCS endpoints for packages and imports directly.

I'm not sure whether it'd be strictly required or not. There are pros and cons to both models—monolithic builds are easier to overview etc, but having a repo for each language support makes for a cleaner interface between each.

I don't know too much about how a TCK should be structured, but it could even be outside of the main cloudstate repository and be together with something that builds "cloudstate + (any user function language support)" is needed. The CloudState "reference implementation" would be always a JVM based one; so keep these implementations together would be natural for a JVM based reference implementation of CloudState.

Ideally we should be able to create a native executable using GraalVM native-image to build the TCK and put it in a Docker container for easy reuse, then it'd only be a matter of parameterizing a docker run to verify an implementation.

I have also not thought too much about the topic, but it came to me as I would have to provide a canonical "godoc" story which assumes to have a separate repository for the go code.

Good point, I was not aware of this :)

@jroper or any other community member might want to weigh in? :)

@marcellanz
Copy link
Contributor Author

marcellanz commented Sep 16, 2019

@viktorklang

I have also not thought too much about the topic, but it came to me as I would have to provide a canonical "godoc" story which assumes to have a separate repository for the go code.

Good point, I was not aware of this :)

This, the godoc story, should not drive such a decision for sure and shouldn't be a priority. It can be done without it but it is more complex than I thought.

There is an old and still open issue about that on Go's side golang/go#2381 (comment) where it is still surprisingly non-trivial to produce, just some "bunch of HTML godoc" for a package.

@akramtexas
Copy link
Contributor

  • I second the option of having each specific language support library (e.g. a cloudstateio/go-support repository) live in its own repo as making the most sense. Among other things, and as @viktorklang succinctly pointed out: "there is less maintenance burden due to making sweeping changes across the board." IMHO, managing repos via the strategy of "divide-and-conquer" can't be beat.
  • Regarding the consideration of whether we assume Go is (natively) installed (versus run it in Docker), I lend my support to Docker-supported Go compilation. To add anecdotal credence to my statement, another open source Linux Foundation project that I've been immersed in for the past 14 months (it's called EdgeX Foundry) relies on Docker-supported Go compilation, which is an option that continues to work efficiently and reliably. Given that this options works well for Go projects that I've worked on (with hundreds of thousands of LOC) gives me confidence that it should work equally well (or perhaps even better) for CloudState, which has a (relatively) modest amount of Go code to compile.

@akramtexas
Copy link
Contributor

akramtexas commented Sep 19, 2019

Here's a snippet, FWIW, from a sample Dockerfile (from the open source Linux Foundation project I referred to above) to illustrate what I have in mind:

RUN apk update && apk add make bash git
COPY go.mod .

COPY go.sum .

RUN go mod download
COPY . .

Build the SMA executable.

RUN make cmd/sys-mgmt-agent/sys-mgmt-agent

Build the golang "executor" executable.

RUN make cmd/sys-mgmt-executor/sys-mgmt-executor

Get the Docker-in-Docker image layered-in now.

FROM docker:latest
RUN apk add --no-cache bash
RUN rm -rf /var/cache/apk/*
...
...

Copy over the SMA executable bits.

COPY --from=builder /go/src/github.com/edgexfoundry/edgex-go/cmd/sys-mgmt-agent/sys-mgmt-agent /
COPY --from=builder /go/src/github.com/edgexfoundry/edgex-go/cmd/sys-mgmt-agent/res/docker/configuration.toml /res/docker/configuration.toml
COPY --from=builder /go/src/github.com/edgexfoundry/edgex-go/cmd/sys-mgmt-agent/res/docker/configuration.toml /res/configuration.toml

Copy over the golang "executor" executable.

COPY --from=builder /go/src/github.com/edgexfoundry/edgex-go/cmd/sys-mgmt-executor/sys-mgmt-executor /
RUN apk --no-cache add py-pip
RUN pip install docker-compose==1.23.2
RUN apk --no-cache add curl
ENTRYPOINT ["/sys-mgmt-agent", "--registry","--profile=docker","--confdir=/res"]

@akramtexas
Copy link
Contributor

@marcellanz - (1) I'm reviewing this PR and appreciate how you've left the history (aka individual commits) intact instead of squashing them; this way, I can easily see how the chain of commits have evolved at a granular level (2) Question: Which version of Go are you using? Is it 1.12.4 by any chance?

@akramtexas 1) I usually let it this way, you can also find the quite hacky first PoC with which I started. 2) I decided to start with 1.13 right away. Thank you for reviewing it. Please consider that the PR is in Draft mode, I have a few tasks I listed above to do. But I appreciate any insights of yours.

@marcellanz This works for me.

  • As they, say, A team, and I recall here from memory one of the wise pontifications by industry luminary Grady Booch, that sticks consistently to a strategy will invariably outperform any other team that doesn't 🏁 Let's do it.
  • I finally found time (in the wee hours of the morning) to review your Go code and will now add comments inline.
  • Overall, great work. I like your Go style and idioms a lot!

}

func (r *EntityDiscoveryResponder) registerFileDescriptor(msg descriptor.Message) error {
fd, _ := descriptor.ForMessage(msg) // this can panic, so we do here
Copy link
Contributor

Choose a reason for hiding this comment

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

@marcellanz Please help me understand your parenthetical note (i.e. // this can panic, so we do here) and in general why we're not performing some sort of error-handling here. I'm trying to reconcile this with some received wisdom, for example, and to give credit where it's due, quoting here from the fine book Go in Practice by Matt Butcher and Matt Farina (Manning Publications), they point out that:

Go assumes that errors will be handled by you, the programmer. If an error occurs and you ignore it, Go doesn’t do anything on your behalf. Not so with panics. When a panic occurs, Go unwinds the stack, looking for handlers for that panic. If no handler is found, Go eventually unwinds all the way to the top of the function stack and stops the program. An unhandled panic will kill your application.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To my regret, I left quite a few Fatal failures left, just so that I got the full attention during start of development for these situations. They're all moved into proper errors with an update to this PR branch.

I also share your concerns regarding exit via log.Fatalf or panic() a program except for very valid reasons at its startup where it can't start at all. Also libraries should not panic, except perhaps internally.

In this case descriptor.ForMessage does panic by itself and also does not visibly states that by its function name, like using MustXXX. So I decided to let it panic until I had a better way to handle it. I fixed it and recover it know and move it into an error registerFileDescriptorwill return.

}

// see EventSourcedServer.Handle
func (esh *EventSourcedHandler) Handle(server protocol.EventSourced_HandleServer) error {
Copy link
Contributor

@akramtexas akramtexas Sep 19, 2019

Choose a reason for hiding this comment

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

This is minor, but I noticed mixed-casing (in EventSourced_HandleServer) of CamelCase 🐫 and snake_case 🐍 - I mention this only because you've taken great care to make casing consistent throughout. And I speak here, in turn, only to the incredibly opinionated theme (the Go way of doing things) that pervades the Go programming paradigm, such as the chafing requirement for everything being inside a GOPATH, not to mention Upper-casing function names and variables in order to export them out of the default package scope 😵

Copy link
Contributor Author

@marcellanz marcellanz Sep 21, 2019

Choose a reason for hiding this comment

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

I fully support consistent naming. In this case these names are generated by the gRPC compiler plugin protoc-gen-go which uses a mix of CamelCase and snake_case identifiers for messages and fields and AFAIK I can't much about it for now.

There is also an issue that addresses this behaviour on the protobuf support project of Go.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see. No worries in that case, we're good for now 👍

if err != nil {
log.Fatalf("unable to Marshal")
}
events = append(events,
Copy link
Contributor

Choose a reason for hiding this comment

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

  • Did you have any concerns regarding the order in which events are emitted in connection with the receiver? Thinking out loud here, as I haven't stepped through the code with a debugger.
  • But thinking specifically to the following two in conjunction: (1) events := esh.marshalEventsTo(entityValue) and (2) the following (trailing) section in func (esh *EventSourcedHandler) marshalEventsTo(entityValue reflect.Value) []*any.Any { ... }:
  	marshal, err := proto.Marshal(message)
  	if err != nil {
  		log.Fatalf("unable to Marshal")
  	}
  	events = append(events,
  		&any.Any{
  			TypeUrl: fmt.Sprintf("%s/%s", protoAnyBase, proto.MessageName(message)),
  			Value:   marshal,
  		},
  	)
  }
  emitter.Clear()

}
return events

Copy link
Contributor Author

@marcellanz marcellanz Sep 24, 2019

Choose a reason for hiding this comment

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

Did you have any concerns regarding the order in which events are emitted

@akramtexas Actually I'm not. This should be completely in sequence of what is stated instruction wise. Since theres is, at the first call shortly discussed, no concurrent data path involved, the EventSourcedHandler has at all times full control over the path how events are flowing around. Do you see any races here?

Copy link
Contributor

Choose a reason for hiding this comment

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

@marcellanz Here, too, you have removed my concern. Looks good. For me, the final word on concurrency (in Go!) is the fine book named “Concurrency in Go” by Katherine Cox-Buday, where she has a helpful tip toward the end of the book. FWIW, like so:

“In Go 1.1, a -race flag was added as a flag for most go commands:
  $ go test -race mypkg    # test the package
  $ go run -race mysrc.go  # compile and run the program
  $ go build -race mycmd   # build the command
  $ go install -race mypkg # install the package
If you’re a developer and all you need is a more reliable way to detect race conditions, this is really all you need to know. One caveat of using the race detector is that the algorithm will only find races that are contained in code that is exercised. For this reason, the Go team recommends running a build of your application built with the race flag under real-world load. This increases the probability of finding races by virtue of increasing the probability that more code is exercised.”

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@akramtexas yeah the -race flag is of great help for detecting races in code. As a precaution I have been using this flag with the go-support/build/run_go_test_in_docker.sh script

go test -count 1 -race ./...

We might even run the TCK with the flag enabled to get covered here.

Copy link
Contributor

Choose a reason for hiding this comment

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

@marcellanz Good to hear about your routine use of the -race flag 👍 And yes, I support running the TCK with the flag enabled to get covered in this area of Go code, provided that it does not create any (infrastructural) burden.

cart := domain.Cart{
Items: make([]*domain.LineItem, 0),
}
cart.Items = append(cart.Items, sc.cart...)
Copy link
Contributor

@akramtexas akramtexas Sep 19, 2019

Choose a reason for hiding this comment

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

Does this need to be variadic instead of being implicitly handled by the Go runtime?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how it can be "implicitly handled by the Go runtime"? – I think thats not possible.

func append(slice []Type, elems ...Type) []Type is variadic, but as far as I know, the type of the second argument, Type, has to be the element type of the first argument of append by the language spec. As sc.cart is of type []*domain.LineItem I have to suffix the second argument with the ellipsis ... to be spread into multiple arguments. This way, I don't have to loop over sc.cart.

Copy link
Contributor

Choose a reason for hiding this comment

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

That makes sense (I was thinking out loud to eliminate a possible source of flaw, but you have satisfied my potential concern, all good here) 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking out loud to eliminate a possible source of flaw

You're most welcome. Please do at any time.

@marcellanz
Copy link
Contributor Author

  • As they, say, A team, and I recall here from memory one of the wise pontifications by industry luminary Grady Booch, that sticks consistently to a strategy will invariably outperform any other team that doesn't 🏁 Let's do it.
  • I finally found time (in the wee hours of the morning) to review your Go code and will now add comments inline.
  • Overall, great work. I like your Go style and idioms a lot!

@akramtexas thank you very much for your time you kindly took to review this PR. I'll have time tomorrow evening to go through your review comments and I'll look forward to any discussions that arise from.

… a potential panic from descriptor.ForMessage
@akramtexas
Copy link
Contributor

akramtexas commented Sep 21, 2019

@marcellanz - The pleasure is all mine 👍

  • While I remain very much a programmer with a decidedly Scala/Java mindset, my immersion in the Go ecosystem (over the past 14 months-plus) has been an enlightening experience in terms of the idioms I have, and continue to, pick up.
  • I hope my review comments will help out.
  • Yes, likewise, as a fellow volunteer-contributor who goes about this in his free time, I, too, am super-excited about helping make CloudState an even more awesome project 🚀

… to run go-support tests in a container, fail properly in bash scripts
host, ok := os.LookupEnv("HOST")
if !ok {
log.Fatalf("unable to get environment variable \"HOST\"")
return fmt.Errorf("unable to get environment variable \"HOST\"")
Copy link
Contributor

Choose a reason for hiding this comment

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

Much more meaningful response here, and elsewhere in your latest update 👍

}
marshal, err := proto.Marshal(message)
if err != nil {
log.Fatalf("unable to Marshal")
return nil, fmt.Errorf("%s, %w", err, ErrMarshal)
Copy link
Contributor

Choose a reason for hiding this comment

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

Excellent use of multi-argument logging. Succinctly done 👍

@akramtexas
Copy link
Contributor

@marcellanz I have added a couple more comments this Sunday morning (based on your latest change-set push). Regarding my set of prior comments, I thank you for addressing them, plus I appreciated your detailed responses, including this one, which, incidentally, makes perfect sense:

func append(slice []Type, elems ...Type) []Type is variadic, but as far as I know, the type of the second argument, Type, has to be the element type of the first argument of append by the language spec. As sc.cart is of type []*domain.LineItem I have to suffix the second argument with the ellipsis ... to be spread into multiple arguments. This way, I don't have to loop over sc.cart.

  • I got your responses via Github-generated emails, but, for some reason, those responses don't appear in this PR (neither under the Conversations tab nor the Files changed tab)...

@marcellanz
Copy link
Contributor Author

marcellanz commented Sep 22, 2019

@akramtexas github hides "outdated" comments. try to find "show outdated" buttons to show them. outdated means, updated by a changing commit.

// The gRPC implementation returns the rpc return method
// and an error as a second return value.
errReturned := called[1]
if errReturned.Interface() != nil && errReturned.Type().Name() == "error" { // FIXME: looks ugly
Copy link
Contributor Author

Choose a reason for hiding this comment

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

calling .Interface() would panic if it cant; better call CanInterface()

@viktorklang
Copy link
Contributor

@marcellanz Great progress here! Would you prefer to have this under its own cloudstateio/go-support repository? I'm thinking we might want to start splitting language supports into their own repos rather soon-ish, to make sure that independent progress can be made. Let me know what you think!

@marcellanz
Copy link
Contributor Author

marcellanz commented Sep 27, 2019

@marcellanz Great progress here! Would you prefer to have this under its own cloudstateio/go-support repository? I'm thinking we might want to start splitting language supports into their own repos rather soon-ish, to make sure that independent progress can be made. Let me know what you think!

@viktorklang I would prefer it yes.

The go related cloudstate documentation would stay within cloudstateio/cloudstate I think and the godoc's then can be served through cloudstateio/go-support via godoc.org, the way Go likes to get api doc delivered.

Then I'll have to figure how to build the TCK part, but it might be just a go get github.com/cloudstateio/go-support/shoppingcart within the TCK build.

For any splitted language-support repo and said in #104 and like these two points above, the splitting affects documentation, tck builds and the sharing of the protocol with its *.proto files.

As we moved the protobuf files out of the go-support branch to have them not duplicated, they'll have to be somewhere for an build of the language repo. I'll look into that and what the options are.
Prefereably I'd not like to curl them during a build, or whats your preference on here?

@viktorklang
Copy link
Contributor

@marcellanz Ok, cool. I'll check with @jroper what we need to set up. CLA validator, CI, access rights to the repo etc.

@viktorklang
Copy link
Contributor

@marcellanz I created this now, I'll have to configure it soon-ish: https://github.com/cloudstateio/go-support

@marcellanz
Copy link
Contributor Author

@marcellanz I created this now, I'll have to configure it soon-ish: https://github.com/cloudstateio/go-support

@viktorklang Cool. I assume this PR then is better not merged here. I'll fork it and prepare its separation from the main repo.

@viktorklang
Copy link
Contributor

@marcellanz Let's try that!

…de), emit events through a subscription. Starting from here, get prepared to be in a dedicated repo for cloudstate.
@marcellanz
Copy link
Contributor Author

@akramtexas as shortly discussed above I will close this PR in the coming days as I'm moving the code into a separate go-support repository. I will reference this then closed PR within a new PR.

The task is captured in this issue on the new repository.

Thank you for your review input so far. I'll looking forward to the initial PR on the go-support repo which will have all input of yours included; I think I will also rebase whats been done so far to have a clean history for the first commit.

@akramtexas
Copy link
Contributor

@marcellanz I, too, support that this PR is better off not being merged here, and instead going with the option of forking and separating (the work done here, to date) from the main repo ✅

I am happy to contribute (and will continue to do so) with review input based on my know-how of Go and its ecosystem 👍

@marcellanz
Copy link
Contributor Author

as in cloudstateio/go-support#1 announcent, I close this PR as it will be moved to its own repository.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Golang support
4 participants