0

I'm trying to deal with a problem (most probably a design one) regarding the usage of Channels and the proper handling of those. I'm using Knative Eventing/Cloud Events to create and eventing pipeline.

I want to be able to handle different channels in order to receive events originating from different sources/methods.

In order to do so, I have the implementation that follows (code has been removed in order to be concise with detailing the issue).

I have a file1.go which defines a EventHandler struct, associated methods and a couple of exported methods (CreatePreview() and SaveAndPublish()) that are the "normal" behaviour of the app and that actually receives/deals with whatever value comes on the Channel:

type EventHandler struct {
  Channel chan string
}

func (ev *EventHandler) Handle(event cloudevents.Event) {
  if event.Data == nil {
    (...)
  }

  var data string
  if err := event.DataAs(&data); err != nil {
    (...)
  }

  ev.Channel <- data

  defer close(ev.Channel)
}

func (ev *EventHandler) Create(param *Element) (error) {
  (...) //Unimportant code
}

func (repo *Repository) CreatePreview(param1 string, param2 string, eventHandler *EventHandler) (*pb.PreviewResponse, error) {
  (...)
  err := eventHandler.Create(&document)
  (...)
  preview := <- eventHandler.Channel
  (...)
}

func (repo *Repository) SaveAndPublish(param1 string, param2 bool, eventHandler *EventHandler) (*pb.PublishResponse, error) {

  (...)
  err := eventHandler.Create(&documentToUpdate)
  (...)

  published := <- eventHandler.Channel
  (...)
  return repo.SomeOtherMethod(published.ID)
}

Now, on my main.go function, I have the "regular" start of a service, including a gRPC Listener, a HTTP Listener and handling of events. This is done via cmux. So here's a sample of the code (again, code simplified):

func HandlerWrapper(event cloudevents.Event) {
    //TODO: HOW DO I HANDLE THIS???
}

// This approach seems to cause issues since it's always the same instance
// var (
//  eventHandler = &rep.EventHandler{Channel: make(chan string)}
// )

func (service *RPCService) Start() (err error) {

  port, err := strconv.Atoi(os.Getenv("LISTEN_PORT"))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  // Create multiplexer and listener types
  mux := cmux.New(lis)
  grpcLis := mux.Match(cmux.HTTP2())
  httpLis := mux.Match(cmux.HTTP1())

  // *************
  // gRPC
  // *************
  service.server = grpc.NewServer()
  reflection.Register(service.server)
  pb.RegisterStoryServiceServer(service.server, service)

  // *************
  // HTTP
  // *************

  // Declare new CloudEvents Receiver
  c, err := kncloudevents.NewDefaultClient(httpLis)
  if err != nil {
    log.Fatal("Failed to create client, ", err)
  }

  // *************
  // Start Listeners
  // *************

  // start gRPC server
  go func() {
    if err := service.server.Serve(grpcLis); err != nil {
      log.Fatalf("failed to gRPC serve: %s", err)
    }
  }()

  // start HTTP server
  go func() {
    // With this line bellow, I'd have to create a Received per eventHandler. Not cool
    // log.Fatal(c.StartReceiver(context.Background(), eventHandler.Handle))

    // Here we use a wrapper to deal with the event handling and have a single listener
    log.Fatal(c.StartReceiver(context.Background(), HandlerWrapper))
  }()

  if err := mux.Serve(); err != nil {
    log.Fatalf("failed to Mux serve: %s", err)
  }

  return
}

//CreatePreview is used to save a preview for a story 
func (service *RPCService) CreatePreview(ctx context.Context, input *pb.PreviewRequest) (*pb.PreviewResponse, error){

  eventHandler := &rep.EventHandler{Channel: make(chan string)}

  story, err := service.repo.CreatePreview("param1", "param2", eventHandler)
  if err != nil {
    return nil, err
  }

  // Return matching the `CreatePreviewResponse` message we created in our
  // protobuf definition.
  return &pb.PreviewResponse{Story: story}, nil
}

// SaveAndPublish is used to save a story and publish it, returning the story saved. 
func (service *RPCService) SaveAndPublish(ctx context.Context, input *pb.PublishRequest) (*pb.PublishResponse, error){

  eventHandler := &rep.EventHandler{Channel: make(chan string)}

  story, err := service.repo.SaveAndPublish("param1", true, eventHandler)

  if err != nil {
    return nil, err
  }

  // Return matching the `SaveAndPublishResponse` message we created in our
  // protobuf definition.
  return &pb.PublishResponse{Story: story}, nil
}

Now, I know that instead of having to instantiate a single, global eventHandler, in order to use the eventHandler.Handle method on c.StartReceiver() on main.go I can define a wrapper that would, maybe, contain a list of eventHandlers (the HandlerWrapper() method on main.go).

However, I do not know how I could identify which instance of an EventHandler is which and how to properly handle and route these operations, and that is my question:

How do I go about this case where I want to create a Wrapper (a single function to pass into c.StartReceive()) and then let it be handled by the correct instance of Handle()?

I hope the question is clear. I've been trying to get my head around this for a couple days already and can't figure out how to do it.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Zed_Blade
  • 1,009
  • 3
  • 18
  • 38

1 Answers1

1

Presumably, you should be able to differentiate events by using the different sources/methods coming from that event. A quick look at the event spec shows you could split into channels based on source, for example.

The main thing I see that isn't being utilized here is the context object. It seems you could glean the source from that context. This can be seen in their hello world example (check out the receive function).

For your example:

// these are all the handlers for the different sources.
type EventHandlers map[string]CloudEventHandler

var _eventHandlerKey = "cloudEventHandlers"

func HandlerWrapper(ctx context.Context, event cloudevents.Event) {
    // Get event source from event context.
    src := event.Context.Source
    // Then get the appropriate handler for that source (attached to context).
    handler := ctx.Value(_eventHandlers).(Values)[src]
    // ex: src = "/foo" 
    handler.SendToChannel(event)
}

func main() {
    eventHandlers := make(map[string]CloudEventHandler)
    // create all the channels we need, add it to the context.
    for _, source := range sourceTypes { // foo bar baz
        handler := NewHandler(source)
        eventHandlers[source] = handler
    }

  // start HTTP server
  go func() {
    // Add the handlers to the context.
    context := context.WithValue(context.Background(), _eventHandlerKey, eventHandlers)
    log.Fatal(c.StartReceiver(context.Background(), HandlerWrapper))
  }
}()

If there are say 3 different sources to be supported, you can use the factory pattern to instantiate those different channels and an interface that all of those implement.

// CloudEventHandler Handles sending cloud events to the proper channel for processing.
type CloudEventHandler interface {
    SendToChannel(cloudEvents.Event)
}

type fooHandler struct {channel chan string}
type barHandler struct {channel chan int}
type bazHandler struct {channel chan bool}

func NewHandler(source string) CloudEventHandler {
    switch source {
    case "/foo":
        return &fooHandler{channel: make(chan string, 2)} // buffered channel
    case "/bar":
        return &barHandler{channel: make(chan int, 2)}
    case "/baz":
        return &bazHandler{channel: make(chan bool, 2)}
    }
}

func (fh *fooHandler) SendToChannel(event CloudEvents.Event) {
    var data string
    if err := event.DataAs(&data); err != nil {
        // (...)
    }

    go func() {
        fh.channel <- data
    }()
}

func (bh *barHandler) SendToChannel(event CloudEvents.Event) {
    var data int
    if err := event.DataAs(&data); err != nil {
        // (...)
    }

    go func() {
        bh.channel <- data
    }()
}
Britt
  • 82
  • 5