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

Added a service Client interface and renamed the server interface #207

Merged
merged 6 commits into from
Mar 5, 2025

Conversation

abjeni
Copy link
Collaborator

@abjeni abjeni commented Mar 2, 2025

Example client interface:

// Raft is the client-side API for the Raft Service
type RaftClient interface {
	RequestVote(ctx context.Context, in *raftpb.RequestVoteRequest) (resp *raftpb.RequestVoteResponse, err error)
	AppendEntries(ctx context.Context, in *raftpb.AppendEntriesRequest) (resp *raftpb.AppendEntriesResponse, err error)
	AppendEntries2(ctx context.Context, in *raftpb.AppendEntriesRequest) (resp *raftpb.AppendEntriesResponse, err error)
	InstallSnapshot(ctx context.Context, in *commonpb.Snapshot) (resp *raftpb.InstallSnapshotResponse, err error)
	CatchMeUp(ctx context.Context, in *raftpb.CatchMeUpRequest) (resp *raftpb.Empty, err error)
}

The server interface would now look like this:

// Raft is the server-side API for the Raft Service
type RaftServer interface {
	RequestVote(ctx gorums.ServerCtx, request *raftpb.RequestVoteRequest) (response *raftpb.RequestVoteResponse, err error)
	AppendEntries(ctx gorums.ServerCtx, request *raftpb.AppendEntriesRequest) (response *raftpb.AppendEntriesResponse, err error)
	AppendEntries2(ctx gorums.ServerCtx, request *raftpb.AppendEntriesRequest) (response *raftpb.AppendEntriesResponse, err error)
	InstallSnapshot(ctx gorums.ServerCtx, request *commonpb.Snapshot) (response *raftpb.InstallSnapshotResponse, err error)
	CatchMeUp(ctx gorums.ServerCtx, request *raftpb.CatchMeUpRequest) (response *raftpb.Empty, err error)
}

Fixes #178

Copy link
Contributor

deepsource-io bot commented Mar 2, 2025

Here's the code health analysis summary for commits a29b7b1..ecb4e77. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource Go LogoGo✅ SuccessView Check ↗
DeepSource Shell LogoShell✅ SuccessView Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

Copy link
Member

@meling meling left a comment

Choose a reason for hiding this comment

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

Nice work!

There is a small snag though. The client interface must match the client-side implementation (in the generated _gorums.pb.go files); specifically, the multicast and unicast types has an extra variadic opts argument.

One way to enforce interface compliance is to declare a variable like this (could be in a separate file in the dev folder):

// enforce interface compliance
var _ ZorumsServiceClient = (*Configuration)(nil)

This will give a compile error if the generated Configuration object does not match the ZorumsServiceClient interface.

@abjeni
Copy link
Collaborator Author

abjeni commented Mar 4, 2025

I had to create two client interfaces, one for Configuration and one for Node, which looks like this:

// Raft is the client-side Configuration API for the Raft Service
type RaftConfigurationClient interface {
	RequestVote(ctx context.Context, in *raftpb.RequestVoteRequest) (resp *raftpb.RequestVoteResponse, err error)
	AppendEntries(ctx context.Context, in *raftpb.AppendEntriesRequest, f func(*raftpb.AppendEntriesRequest, uint32) *raftpb.AppendEntriesRequest) (resp *raftpb.AppendEntriesQFResponse, err error)
}

// enforce interface compliance
var _ RaftConfigurationClient = (*Configuration)(nil)

// Raft is the client-side Node API for the Raft Service
type RaftNodeClient interface {
	AppendEntries2(ctx context.Context, in *raftpb.AppendEntriesRequest) (resp *raftpb.AppendEntriesResponse, err error)
	InstallSnapshot(ctx context.Context, in *commonpb.Snapshot) (resp *raftpb.InstallSnapshotResponse, err error)
	CatchMeUp(ctx context.Context, in *raftpb.CatchMeUpRequest) (resp *raftpb.Empty, err error)
}

// enforce interface compliance
var _ RaftNodeClient = (*Node)(nil)

Copy link
Member

@meling meling left a comment

Choose a reason for hiding this comment

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

Nice work again... We should avoid creating empty interfaces for both Node and Configuration.


var clientNodeInterface = `
{{$genFile := .GenFile}}
{{range .Services -}}
Copy link
Member

Choose a reason for hiding this comment

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

See below for func definitions; use like this:

{{- range nodeOnlyServices .Services}}

{{$service := .GoName}}
// {{$service}} is the client-side Node API for the {{$service}} Service
type {{$service}}NodeClient interface {
{{- range .Methods}}
Copy link
Member

Choose a reason for hiding this comment

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

And this:

{{- range nodeMethods .Methods}}


var clientConfigurationInterface = `
{{$genFile := .GenFile}}
{{range .Services -}}
Copy link
Member

Choose a reason for hiding this comment

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

{{- range gorumsServices .Services}}

Copy link
Member

Choose a reason for hiding this comment

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

(see implementations of the helper funcs below)

},
"isConfigurationCall": func(method *protogen.Method) bool {
return hasMethodOption(method, gorums.E_Multicast, gorums.E_Quorumcall, gorums.E_Correctable, gorums.E_Async)
},
"methods": func(services []*protogen.Service) (methods []*protogen.Method) {
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps we can remove this one; it does not appear to be used. I don't remember why I added it, and it seems we are using .Methods instead.

var _ BenchmarkConfigurationClient = (*Configuration)(nil)

// Benchmark is the client-side Node API for the Benchmark Service
type BenchmarkNodeClient interface {
Copy link
Member

Choose a reason for hiding this comment

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

We should avoid generating empty interfaces, and the corresponding interface compliance check if it is empty. See other comments for suggestions.

)

// ZorumsService is the client-side Configuration API for the ZorumsService Service
type ZorumsServiceConfigurationClient interface {
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we want such a long name... What do you think about just calling it ZorumsServiceClient to align with gRPC's naming?

We can still use ZorumsServiceNodeClient since that's more of a special case for Gorums.

{{- range .Methods}}
{{- if isConfigurationCall .}}
{{- if isOneway .}}
{{- $customOut := customOut $genFile .}}
Copy link
Member

Choose a reason for hiding this comment

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

This one does not appear to be needed since $customOut isn't used in this if-branch.

{{- range .Methods}}
{{- if not (isConfigurationCall .)}}
{{- if isOneway .}}
{{- $customOut := customOut $genFile .}}
Copy link
Member

Choose a reason for hiding this comment

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

This one does not appear to be needed since $customOut isn't used in this if-branch.

@abjeni
Copy link
Collaborator Author

abjeni commented Mar 4, 2025

the qspecServices does not do what it was supposed to do. I decided to remove it since we need a QuorumSpec interface for the Configuration struct anyway.

Copy link
Member

@meling meling left a comment

Choose a reason for hiding this comment

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

A few more changes please. Mostly doc comment improvements and interface naming. And recall to regenerate.


var client = clientVariables + clientConfigurationInterface + clientNodeInterface

// gorumsMethods returns all Gorums-specific methods, such as multicast, quorumcall, correctable, and async methods.
Copy link
Member

Choose a reason for hiding this comment

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

gorumsMethods --> configurationMethods

return s
}

// nodeOnlyServices returns all node-only services, such as services with only unicast and plain gRPC methods.
Copy link
Member

Choose a reason for hiding this comment

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

nodeOnlyServices? Or nodeServices.

return s
}

// gorumsServices returns all services that have Gorums methods.
Copy link
Member

Choose a reason for hiding this comment

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

configurationServices

{{- range configurationsServices .Services}}
{{$service := .GoName}}
// {{$service}} is the client-side Configuration API for the {{$service}} Service
type {{$service}}ConfigurationClient interface {
Copy link
Member

Choose a reason for hiding this comment

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

// {{$service}}Client is the client interface for the {{$service}} service.
type {{$service}}Client interface {

I feel that having the Configuration name be part of the interface name gives it a connotation to something we can configure.

{{$genFile := .GenFile}}
{{- range nodeServices .Services}}
{{$service := .GoName}}
// {{$service}} is the client-side Node API for the {{$service}} Service
Copy link
Member

Choose a reason for hiding this comment

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

// {{$service}}NodeClient is the single node client interface for the {{$service}} service.
type {{$service}}NodeClient interface {

(only comment change)

@abjeni
Copy link
Collaborator Author

abjeni commented Mar 5, 2025

I rewrote some of the comments and fixed the qspec template, now you could maybe even have multiple services without quorum calls.
renamed ServiceConfigurationClient to ConfigurationClient.

@meling meling merged commit 4c3bde2 into master Mar 5, 2025
5 checks passed
@meling meling deleted the Salhus/issue178/generate_ServiceNameClient_interface branch March 5, 2025 15:21
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.

feat: Generate ServiceNameClient interface akin to gRPC's plugin
2 participants