How to: Implement pluggable components

Learn how to author and implement pluggable components

In this guide, you’ll learn why and how to implement a pluggable component. To learn how to configure and register a pluggable component, refer to How to: Register a pluggable component

Implement a pluggable component

In order to implement a pluggable component, you need to implement a gRPC service in the component. Implementing the gRPC service requires three steps:

Find the proto definition file

Proto definitions are provided for each supported service interface (state store, pub/sub, bindings).

Currently, the following component APIs are supported:

  • State stores
  • Pub/sub
  • Bindings
ComponentTypegRPC definitionBuilt-in Reference ImplementationDocs
State Storestate[state.proto]Redisconcept, howto, api spec
Pub/subpubsub[pubsub.proto]Redisconcept, howto, api spec
Bindingsbindings[bindings.proto]Kafkaconcept, input howto, output howto, api spec

Below is a snippet of the gRPC service definition for pluggable component state stores ([state.proto]):

  1. // StateStore service provides a gRPC interface for state store components.
  2. service StateStore {
  3. // Initializes the state store component with the given metadata.
  4. rpc Init(InitRequest) returns (InitResponse) {}
  5. // Returns a list of implemented state store features.
  6. rpc Features(FeaturesRequest) returns (FeaturesResponse) {}
  7. // Ping the state store. Used for liveness purposes.
  8. rpc Ping(PingRequest) returns (PingResponse) {}
  9. // Deletes the specified key from the state store.
  10. rpc Delete(DeleteRequest) returns (DeleteResponse) {}
  11. // Get data from the given key.
  12. rpc Get(GetRequest) returns (GetResponse) {}
  13. // Sets the value of the specified key.
  14. rpc Set(SetRequest) returns (SetResponse) {}
  15. // Deletes many keys at once.
  16. rpc BulkDelete(BulkDeleteRequest) returns (BulkDeleteResponse) {}
  17. // Retrieves many keys at once.
  18. rpc BulkGet(BulkGetRequest) returns (BulkGetResponse) {}
  19. // Set the value of many keys at once.
  20. rpc BulkSet(BulkSetRequest) returns (BulkSetResponse) {}
  21. }

The interface for the StateStore service exposes a total of 9 methods:

  • 2 methods for initialization and components capability advertisement (Init and Features)
  • 1 method for health-ness or liveness check (Ping)
  • 3 methods for CRUD (Get, Set, Delete)
  • 3 methods for bulk CRUD operations (BulkGet, BulkSet, BulkDelete)

Create service scaffolding

Use protocol buffers and gRPC tools to create the necessary scaffolding for the service. Learn more about these tools via the gRPC concepts documentation.

These tools generate code targeting any gRPC-supported language. This code serves as the base for your server and it provides:

  • Functionality to handle client calls
  • Infrastructure to:
    • Decode incoming requests
    • Execute service methods
    • Encode service responses

The generated code is incomplete. It is missing:

  • A concrete implementation for the methods your target service defines (the core of your pluggable component).
  • Code on how to handle Unix Socket Domain integration, which is Dapr specific.
  • Code handling integration with your downstream services.

Learn more about filling these gaps in the next step.

Define the service

Provide a concrete implementation of the desired service. Each component has a gRPC service definition for its core functionality which is the same as the core component interface. For example:

  • State stores

    A pluggable state store must provide an implementation of the StateStore service interface.

    In addition to this core functionality, some components might also expose functionality under other optional services. For example, you can add extra functionality by defining the implementation for a QueriableStateStore service and a TransactionalStateStore service.

  • Pub/sub

    Pluggable pub/sub components only have a single core service interface defined ([pubsub.proto]). They have no optional service interfaces.

  • Bindings

    Pluggable input and output bindings have a single core service definition on [bindings.proto]. They have no optional service interfaces.

After generating the above state store example’s service scaffolding code using gRPC and protocol buffers tools, you can define concrete implementations for the 9 methods defined under service StateStore, along with code to initialize and communicate with your dependencies.

This concrete implementation and auxiliary code are the core of your pluggable component. They define how your component behaves when handling gRPC requests from Dapr.

Returning semantic errors

Returning semantic errors are also part of the pluggable component protocol. The component must return specific gRPC codes that have semantic meaning for the user application, those errors are used to a variety of situations from concurrency requirements to informational only.

ErrorgRPC error codeSource componentDescription
ETag Mismatchcodes.FailedPreconditionState storeError mapping to meet concurrency requirements
ETag Invalidcodes.InvalidArgumentState store
Bulk Delete Row Mismatchcodes.InternalState store

Learn more about concurrency requirements in the State Management overview.

The following examples demonstrate how to return an error in your own pluggable component, changing the messages to suit your needs.

Important: In order to use .NET for error mapping, first install the Google.Api.CommonProtos NuGet package.

Etag Mismatch

  1. var badRequest = new BadRequest();
  2. var des = "The ETag field provided does not match the one in the store";
  3. badRequest.FieldViolations.Add(
  4. new Google.Rpc.BadRequest.Types.FieldViolation
  5. {
  6. Field = "etag",
  7. Description = des
  8. });
  9. var baseStatusCode = Grpc.Core.StatusCode.FailedPrecondition;
  10. var status = new Google.Rpc.Status{
  11. Code = (int)baseStatusCode
  12. };
  13. status.Details.Add(Google.Protobuf.WellKnownTypes.Any.Pack(badRequest));
  14. var metadata = new Metadata();
  15. metadata.Add("grpc-status-details-bin", status.ToByteArray());
  16. throw new RpcException(new Grpc.Core.Status(baseStatusCode, "fake-err-msg"), metadata);

Etag Invalid

  1. var badRequest = new BadRequest();
  2. var des = "The ETag field must only contain alphanumeric characters";
  3. badRequest.FieldViolations.Add(
  4. new Google.Rpc.BadRequest.Types.FieldViolation
  5. {
  6. Field = "etag",
  7. Description = des
  8. });
  9. var baseStatusCode = Grpc.Core.StatusCode.InvalidArgument;
  10. var status = new Google.Rpc.Status
  11. {
  12. Code = (int)baseStatusCode
  13. };
  14. status.Details.Add(Google.Protobuf.WellKnownTypes.Any.Pack(badRequest));
  15. var metadata = new Metadata();
  16. metadata.Add("grpc-status-details-bin", status.ToByteArray());
  17. throw new RpcException(new Grpc.Core.Status(baseStatusCode, "fake-err-msg"), metadata);

Bulk Delete Row Mismatch

  1. var errorInfo = new Google.Rpc.ErrorInfo();
  2. errorInfo.Metadata.Add("expected", "100");
  3. errorInfo.Metadata.Add("affected", "99");
  4. var baseStatusCode = Grpc.Core.StatusCode.Internal;
  5. var status = new Google.Rpc.Status{
  6. Code = (int)baseStatusCode
  7. };
  8. status.Details.Add(Google.Protobuf.WellKnownTypes.Any.Pack(errorInfo));
  9. var metadata = new Metadata();
  10. metadata.Add("grpc-status-details-bin", status.ToByteArray());
  11. throw new RpcException(new Grpc.Core.Status(baseStatusCode, "fake-err-msg"), metadata);

Just like the Dapr Java SDK, the Java Pluggable Components SDK uses Project Reactor, which provides an asynchronous API for Java.

Errors can be returned directly by:

  1. Calling the .error() method in the Mono or Flux that your method returns
  2. Providing the appropriate exception as parameter.

You can also raise an exception, as long as it is captured and fed back to your resulting Mono or Flux.

ETag Mismatch

  1. final Status status = Status.newBuilder()
  2. .setCode(io.grpc.Status.Code.FAILED_PRECONDITION.value())
  3. .setMessage("fake-err-msg-for-etag-mismatch")
  4. .addDetails(Any.pack(BadRequest.FieldViolation.newBuilder()
  5. .setField("etag")
  6. .setDescription("The ETag field provided does not match the one in the store")
  7. .build()))
  8. .build();
  9. return Mono.error(StatusProto.toStatusException(status));

ETag Invalid

  1. final Status status = Status.newBuilder()
  2. .setCode(io.grpc.Status.Code.INVALID_ARGUMENT.value())
  3. .setMessage("fake-err-msg-for-invalid-etag")
  4. .addDetails(Any.pack(BadRequest.FieldViolation.newBuilder()
  5. .setField("etag")
  6. .setDescription("The ETag field must only contain alphanumeric characters")
  7. .build()))
  8. .build();
  9. return Mono.error(StatusProto.toStatusException(status));

Bulk Delete Row Mismatch

  1. final Status status = Status.newBuilder()
  2. .setCode(io.grpc.Status.Code.INTERNAL.value())
  3. .setMessage("fake-err-msg-for-bulk-delete-row-mismatch")
  4. .addDetails(Any.pack(ErrorInfo.newBuilder()
  5. .putAllMetadata(Map.ofEntries(
  6. Map.entry("affected", "99"),
  7. Map.entry("expected", "100")
  8. ))
  9. .build()))
  10. .build();
  11. return Mono.error(StatusProto.toStatusException(status));

ETag Mismatch

  1. st := status.New(codes.FailedPrecondition, "fake-err-msg")
  2. desc := "The ETag field provided does not match the one in the store"
  3. v := &errdetails.BadRequest_FieldViolation{
  4. Field: etagField,
  5. Description: desc,
  6. }
  7. br := &errdetails.BadRequest{}
  8. br.FieldViolations = append(br.FieldViolations, v)
  9. st, err := st.WithDetails(br)

ETag Invalid

  1. st := status.New(codes.InvalidArgument, "fake-err-msg")
  2. desc := "The ETag field must only contain alphanumeric characters"
  3. v := &errdetails.BadRequest_FieldViolation{
  4. Field: etagField,
  5. Description: desc,
  6. }
  7. br := &errdetails.BadRequest{}
  8. br.FieldViolations = append(br.FieldViolations, v)
  9. st, err := st.WithDetails(br)

Bulk Delete Row Mismatch

  1. st := status.New(codes.Internal, "fake-err-msg")
  2. br := &errdetails.ErrorInfo{}
  3. br.Metadata = map[string]string{
  4. affected: "99",
  5. expected: "100",
  6. }
  7. st, err := st.WithDetails(br)

Next steps

Last modified June 19, 2023: Merge pull request #3565 from dapr/aacrawfi/skip-secrets-close (b1763bf)