gRPC API
The service is acor.server.v1.Acor, defined in
server/proto/acor/v1/acor.proto.
It mirrors the HTTP API one for one. The main that serves it is
Running a Server.
The
acor/servermodule is experimental and separately versioned — this service definition can change in any release.
Methods
| RPC | Request | Response |
|---|---|---|
Add | KeywordRequest{keyword} | CountResponse{count} |
Remove | KeywordRequest{keyword} | CountResponse{count} |
Find | InputRequest{input} | MatchesResponse{matches} |
FindIndex | InputRequest{input} | MatchIndexesResponse{matches} |
Suggest | InputRequest{input} | MatchesResponse{matches} |
SuggestIndex | InputRequest{input} | MatchIndexesResponse{matches} |
Info | EmptyRequest | InfoResponse{keywords, nodes} |
Flush | EmptyRequest | StatusResponse{status} |
Status | EmptyRequest | VersionedStatusResponse{status, versions, refresh state} |
All methods are unary; full method names are /acor.server.v1.Acor/<RPC>. Status and the
V3-only methods are served by server.NewVersionedGRPCServer and are unimplemented on the
legacy server. V3 adds VersionedFind, VersionedScan, VersionedMask,
VersionedReplaceText, five expected-version write RPCs, VersionedWait, and
ResolveOperation. Their wire fields mirror the V3 HTTP routes.
Two shapes differ from HTTP:
Counts are
int64.CountResponse.count,InfoResponse.keywords, andInfoResponse.nodesareint64on the wire, where the Go and JSON APIs useint.Match offsets are wrapped. proto3 maps cannot hold a repeated value, so
MatchIndexesResponse.matchesismap<string, Positions>:message Positions { repeated int64 positions = 1; }A Go client reads
resp.GetMatches()["redis"].GetPositions().
Positions are rune offsets, and SuggestIndex always answers [0] because it matches the
input as a prefix. Both behaviors come from the collection rather than the transport, so
they are identical on both surfaces — the HTTP page works through them with
examples.
Errors
Legacy RPC errors remain codes.Internal. V3 maps invalid input and limits to
InvalidArgument, expected-version conflicts to Aborted, missing operation receipts to
NotFound, and maintenance or a closed collection to Unavailable. An ambiguous V3 commit
returns a normal VersionedWriteResponse with outcome == "unknown" and an operation ID;
recover it with ResolveOperation rather than retrying the write.
Legacy deadlines do not cancel the work
For legacy RPCs, a client deadline or disconnect ends the RPC, not the Redis operation behind it.
Service declares no context on any method (server/grpc.go calls
s.service.Add(req.GetKeyword())), and (*AhoCorasick).Add runs against the collection’s
own long-lived context. The request context is accepted by each adapter and discarded.
So a client that gives up on Add, Remove, or Flush and sees DeadlineExceeded has
not prevented the write:
- Do not treat a timeout as “it did not happen”. Re-read with
InfoorFindbefore retrying anything that mutates. - Retries can double-apply.
AddandRemoveare idempotent by keyword; a retriedFlushis a second flush. - A short deadline sheds no server load.
The legacy HTTP API behaves identically. V3 RPCs and HTTP requests pass their request context into the collection. An interrupted V3 write can still have an ambiguous commit; recover its operation receipt instead of assuming it did not happen.
Constructors
server.NewGRPCServer(service, opts...) // bare
server.NewGRPCServerWithObservability(ctx, service, obs, opts...) // + observability
server.NewVersionedGRPCServer(v3, opts...) // V3 API
server.NewVersionedGRPCServerWithObservability(v3, v3obs, opts...) // V3 API + collection-scoped gauges
The V3 observability constructor returns (*grpc.Server, error) and requires
v3obs := &server.VersionedObservability{Metrics: registry, Collection: "moderation-prod"}.
Its gauges refresh at scrape time. The deprecated NewVersionedGRPCServerWithMetrics
constructor ignores its registry argument.
These constructors accept any grpc.ServerOption, including grpc.Creds for TLS, and leave
Serve/Stop to you. Observability bundles four pillars, and any field may be nil to
skip it:
| Field | Wires in | Notes |
|---|---|---|
Tracer | otelgrpc stats handler | Configure with tracing.NewTracer |
Metrics | grpc_server_* Prometheus interceptor | gRPC has no /metrics route — expose promhttp.Handler() on a separate listener |
Logger | zerolog unary interceptor | JSON request logs |
Health | the standard grpc.health.v1 service | See below |
Metric names and tracing configuration are in Operations → Monitoring.
Health checking
Passing Health registers the standard grpc.health.v1.Health service, so
grpc_health_probe and Kubernetes gRPC probes work with no extra code. Four things to know
before setting a probe interval:
- Status is polled, not computed per request. A gRPC probe reads whatever the last poll left behind, so probing more often than every 5 seconds buys no resolution.
- The 5-second tick bounds neither staleness nor load. The poller calls
checker.Check()inline on one long-livedtime.Ticker, which keeps ticking while a check is blocked and buffers one tick — so a check that overruns the interval is followed immediately by the next. A checker taking 30 seconds makes the status at least 30 seconds stale and polls back-to-back for as long as it stays slow, hitting Redis hardest when Redis is already the slow part. A checker that hangs outright cannot act on context cancellation either, so shutdown stops marking the serverNOT_SERVING. Give every checker its own deadline — Running a Server uses 2 seconds. - A nil
Healthfield means no health service at all, so a probe fails withUNIMPLEMENTED, notSERVING(server/grpc.go:77). For a service that always answersSERVING, pass an emptyhealth.NewChecker(); the nil-checker-means-SERVINGrule lives insideRegisterGRPCHealthServerand is reachable only by calling that yourself. - The
ctxpassed toNewGRPCServerWithObservabilitybounds the poller. Cancelling it marks the serverNOT_SERVING, which is what lets a load balancer drain connections beforeGracefulStopfinishes.
Both the overall server ("") and acor.server.v1.Acor are tracked, and they carry the
same status.
Generating a client
There is no buf.yaml or protoc configuration in this repository.
Go clients import the generated package and skip codegen entirely:
import acorv1 "github.com/skyoo2003/acor/server/proto/acor/v1" client := acorv1.NewAcorClient(conn)Other languages run their own
protoc/bufagainstacor.proto. It has no imports beyond proto3, so the single file is the whole input — but--<lang>_outalone generates only the message types. The service stub needs that language’s gRPC plugin as a second output flag:# Python: --python_out gives acor_pb2.py only; the stub needs grpcio-tools. python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. acor.proto # C++/Java/etc. take the plugin the same way: protoc -I. --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=$(which grpc_cpp_plugin) acor.proto