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

Replace gogo protobuf #1256

Merged
merged 46 commits into from
Nov 21, 2023

Conversation

tdeebswihart
Copy link
Contributor

@tdeebswihart tdeebswihart commented Oct 3, 2023

What was changed?

gogo/protobuf has been replaced with Google's official go compiler.

Why?

gogo/protobuf has been deprecated for some time and the community is moving on, building new tools (like vtproto) atop google's v2 compiler.

Breaking changes

  • *time.Time in proto structs will now be timestamppb.Timestamp
  • *time.Duration will now be durationpb.Duration
  • V2-generated structs embed locks, so you cannot dereference them willy-nilly. go vet will scream at you about this
  • Proto enums will, when formatted to JSON, now be in SCREAMING_SNAKE_CASE rather than PascalCase. We decided (in discussion with the SDK team) that now was as good a time as any to rip the bandage off.

Note that history loading will not be impacted by the JSON changes: I rewrote history loading to dynamically fix incoming history JSON data (like all our other sdks); you can find this code in my fork of our go API alongside its tests.

How did you test it?

All tests for this repo pass as do all SDK tests (except for integration tests, which time out on our official repo on master...).

Potential risks

All errors that could arise from this change should be compile-time errors, so your build will be broken if I haven't yet addressed your code. I plan to port all relevant temporal repos as a part of this effort.

Checklist

  1. Closes gogo/protobuf is unmaintained #658
  2. How was this tested: all sdk tests pass (with the exception of integration tests, many of which time out on our official unaltered repo)
  3. Any docs updates needed? Yes. Once all of these changes are merged our history export format will be incompatible with older SDKs due to the JSON changes; we will need to inform customers of this

Release Plan

Our goal is to release our API, Go API, Go SDK, and Server all at once, then update our UI (and ui-server), CLI, and HTTP API in a second pass. Until the second pass happens no changes to our JSON formatting will be apparent to users

go.mod Outdated Show resolved Hide resolved
internal/common/serializer/jsonpb.go Outdated Show resolved Hide resolved
)

// SerializeBatchEvents serializes batch events into a datablob proto
func SerializeBatchEvents(events []*historypb.HistoryEvent, encodingType enumspb.EncodingType) (*commonpb.DataBlob, error) {
return serialize(&historypb.History{Events: events}, encodingType)
}

func serializeProto(p proto.Marshaler, encodingType enumspb.EncodingType) (*commonpb.DataBlob, error) {
func serializeProto(p Marshaler, encodingType enumspb.EncodingType) (*commonpb.DataBlob, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Would also accept proto.Message here too (since this is private and it's only on our protos), but what you have here is fine too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we accept things with a Marshal method then I can sneak vtprotobuf under the hood by swapping out the implementation of Marshal. That way nothing else needs to care about what we're using

Copy link
Member

@cretz cretz Oct 9, 2023

Choose a reason for hiding this comment

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

That is only for this one part, it's not like you'll "sneak" it in everywhere. Are there plans to discourage the use of proto.Marshal for this one reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll find out: once we have the server working we'll run E2E load and see if there's a measurable performance difference. If "No" (our assumption) then no and I'll skip including vtproto

internal/error.go Outdated Show resolved Hide resolved
internal/grpc_dialer_test.go Outdated Show resolved Hide resolved
internal/workflow.go Outdated Show resolved Hide resolved
internal/internal_activity.go Outdated Show resolved Hide resolved
internal/internal_event_handlers.go Outdated Show resolved Hide resolved
internal/internal_schedule_client.go Outdated Show resolved Hide resolved
)

var ErrProtoNameNotFound = errors.New("protocol name not found")

// NameFromMessage extracts the name of the protocol to which the supplied
// message belongs.
func NameFromMessage(msg *protocolpb.Message) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we changing the behavior 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.

A quick and uninformed decision while first learning the proto library. Looks like the anypb.Any type provides MessageName() which would let us have the same behavior

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also if anypb.Body.MessageName() doesn't return a message name then we've no name to report.

The old types package doesn't work with google protobuf; if we want the same semantics I'd probably have to allocate and do something like

_, err := msg.Body.UnmarshalNew()
if err != nil {
   return "", fmt.Errorf("unrecognized message type %q: %w", msg.Body.MessageName(), err)
}

Copy link
Member

@cretz cretz left a comment

Choose a reason for hiding this comment

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

Overall looks good, just very minor comments before I hit approve

@@ -77,3 +77,5 @@ jobs:
go-repo-path: ${{github.event.pull_request.head.repo.full_name}}
version: ${{github.event.pull_request.head.ref}}
version-is-repo-ref: true
features-repo-path: "tdeebswihart/temporal-features"
Copy link
Member

Choose a reason for hiding this comment

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

Goes without saying, just making reminder to update this and similar places when temporalio/api-go#119 is merged

client/client.go Outdated Show resolved Hide resolved
@@ -45,7 +48,7 @@ func anyToString(d interface{}) string {
buf.WriteString("(")
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if f.Kind() == reflect.Invalid {
if f.Kind() == reflect.Invalid || privateField.MatchString(t.Field(i).Name) {
Copy link
Member

Choose a reason for hiding this comment

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

A bit overkill to use regex just to check if the first character of a string is lowercase, but meh

internal/internal_event_handlers.go Show resolved Hide resolved
@@ -28,6 +28,9 @@ import (
"fmt"
"time"

"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"

Copy link
Member

Choose a reason for hiding this comment

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

Nit - while some of the older SDK and server code required developers to manually break up non-stdlib sections by same module and separate module, modern SDK rules do not (but harmless for the most part).

internal/internal_worker.go Outdated Show resolved Hide resolved
internal/internal_worker_test.go Outdated Show resolved Hide resolved
)

var ErrProtoNameNotFound = errors.New("protocol name not found")
Copy link
Member

Choose a reason for hiding this comment

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

Meh, we often don't make exported vars for singly used never otherwise-referenced errors, but no big deal

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'm a fan of them as you can use them with errors.Is, and I prefer that to string matching.

I'm happy to yield to our conventions however

Copy link
Member

@cretz cretz Oct 30, 2023

Choose a reason for hiding this comment

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

We don't really have a convention here, so it's fine. I'm a fan of them when I need to use them with errors.Is, but not for every error in the SDK just because some future user may need to use them with errors.Is. Hence the "never otherwise-referenced". By that logic, we'd never have anymore inline errors and we'd have a huge API surface which I think is a bit too hardcore of a stance to take here (though may have value for other projects).

(again, no need to change)

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've been burnt by Go' stdlib refusing to expose internal networking errors before and have made a habit of it, heh. I'll keep that in mind for future errors I add however

Copy link
Member

@cretz cretz left a comment

Choose a reason for hiding this comment

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

Approving, but please do not merge until dependencies are merged/tagged

This does not yet compile as I removed the proxy. I also still need to
implement history cleaning; SDKs should still load old histories that
have CamelCase enums
We've swapped the roles of GoV2 and gogo here, so we forcibly include a
gogo-generated message definition.
We'll want that functionality in the CLI as well so everything has been
moved to a common repo.
I need to see whether the changes will impact our metrics; it's possible
given what I found in the grpc_dialer tests
We'll want these in other repos as well so I've moved and renamed them.
duration.Proto is clearer than common.DurationPtr and flows better.

Refactoring automated using [gopatch]():

```diff
@@
@@

+import "go.temporal.io/api/types/duration"

-common.DurationValue(...)
+duration.Value(...)

@@
@@

+import "go.temporal.io/api/types/duration"

-common.DurationPtr(...)
+duration.Proto(...)

@@
@@

+import "go.temporal.io/api/types/duration"

-common.MinDurationPtr(...)
+duration.MinProto(...)

@@
@@

+import "go.temporal.io/api/types/timestamp"

-common.TimeValue(...)
+timestamp.Value(...)

@@
@@

+import "go.temporal.io/api/types/timestamp"

-common.TimePtr(...)
+timestamp.Proto(...)
```
This way CI can run and others can function
jsonpb is dead, long live jsonpb!
Our error messages and metric tags are back to their old values, so our
users shouldn't notice that we changed our proto library.
@tdeebswihart tdeebswihart enabled auto-merge (squash) November 21, 2023 22:12
@tdeebswihart tdeebswihart merged commit f9d73bf into temporalio:master Nov 21, 2023
8 checks passed
nichtverstehen pushed a commit to nscloud-demo/temporalio-features that referenced this pull request Nov 22, 2023
**What changed?**

gogo/protobuf has been replaced with Google's official go compiler. 

**Why?**

gogo/protobuf has been deprecated for some time and the community is moving on, building new tools (like vtproto) atop google's v2 compiler.

**How did you test it?**

This branch is actively used by my [SDK](temporalio/sdk-go#1256) and [server](temporalio/temporal#5032) PRs so you can see it in use there

**Potential risks**

None I can think of.

**Breaking changes for developers**

- `*time.Time` in proto structs will now be [timestamppb.Timestamp](https://pkg.go.dev/google.golang.org/[email protected]/types/known/timestamppb#section-documentation)
- `*time.Duration` will now be [durationpb.Duration](https://pkg.go.dev/google.golang.org/protobuf/types/known/durationpb)
- V2-generated structs embed locks, so you cannot dereference them. `go vet` will scream at you about this. If you need a copy, use `proto.Clone`.
- Proto objects, or objects embedding protos, cannot be compared using `reflect.DeepEqual` or _anything_ that uses it. This includes `testify` and `mock` equality testers! 
  - You will need to use the `protorequire` ro `protoassert`packages instead. I've implemented proto-compatible assertions there for all cases I've encountered
  - If you need `reflect.DeepEqual` for any reason you can use `go.temporal.io/api/temporalproto.DeepEqual` instead
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.

gogo/protobuf is unmaintained
3 participants