|
2 | 2 |
|
3 | 3 | Grafana uses a _bus_ to pass messages between different parts of the application. All communication over the bus happens synchronously.
|
4 | 4 |
|
5 |
| -> **Deprecated:** The bus has officially been deprecated, however, we're still using the command/query objects paradigms. |
| 5 | +## Commands and queries |
6 | 6 |
|
7 |
| -There are three types of messages: _events_, _commands_, and _queries_. |
| 7 | +Grafana structures arguments to [services](services.md) using a command/query |
| 8 | +separation where commands are instructions for a mutation and queries retrieve |
| 9 | +records from a service. |
8 | 10 |
|
9 |
| -## Events |
| 11 | +Services should define their methods as `func[T, U any](ctx context.Context, args T) (U, error)`. |
10 | 12 |
|
11 |
| -An event is something that happened in the past. Since an event has already happened, you can't change it. Instead, you can react to events by triggering additional application logic to be run, whenever they occur. |
| 13 | +Each function should take two arguments. First, a `context.Context` that |
| 14 | +carries information about the tracing span, cancellation, and similar |
| 15 | +runtime information that might be relevant to the call. Secondly, `T` is |
| 16 | +a `struct` defined in the service's root package (see the instructions |
| 17 | +for [package hierarchy](package-hierarchy.md)) that contains zero or |
| 18 | +more arguments that can be passed to the method. |
12 | 19 |
|
13 |
| -> Because they happened in the past, event names are written in past tense, such as `UserCreated`, and `OrgUpdated`. |
14 |
| -
|
15 |
| -### Subscribe to an event |
16 |
| - |
17 |
| -In order to react to an event, you first need to _subscribe_ to it. |
18 |
| - |
19 |
| -To subscribe to an event, register an _event listener_ in the service's `Init` method: |
| 20 | +The return values is more flexible, and may consist of none, one, or two |
| 21 | +values. If there are two values returned, the second value should be |
| 22 | +either an `bool` or `error` indicating the success or failure of the |
| 23 | +call. The first value `U` carries a value of any exported type that |
| 24 | +makes sense for the service. |
20 | 25 |
|
21 |
| -```go |
22 |
| -func (s *MyService) Init() error { |
23 |
| - s.bus.AddEventListener(s.UserCreated) |
24 |
| - return nil |
25 |
| -} |
| 26 | +Following is an example of an interface providing method signatures for |
| 27 | +some calls adhering to these guidelines: |
26 | 28 |
|
27 |
| -func (s *MyService) UserCreated(event *events.UserCreated) error { |
28 |
| - // ... |
| 29 | +``` |
| 30 | +type Alphabetical interface { |
| 31 | + // GetLetter returns either an error or letter. |
| 32 | + GetLetter(context.Context, GetLetterQuery) (Letter, error) |
| 33 | + // ListCachedLetters cannot fail, and doesn't return an error. |
| 34 | + ListCachedLetters(context.Context, ListCachedLettersQuery) Letters |
| 35 | + // DeleteLetter doesn't have any return values other than errors, so it |
| 36 | + // returns only an error. |
| 37 | + DeleteLetter(context.Contxt, DeleteLetterCommand) error |
29 | 38 | }
|
30 | 39 | ```
|
31 | 40 |
|
32 |
| -**Tip:** Browse the available events in the `events` package. |
| 41 | +> Because we request an operation to be performed, command are written in imperative mood, such as `CreateFolderCommand`, `GetDashboardQuery` and `DeletePlaylistCommand`. |
33 | 42 |
|
34 |
| -### Publish an event |
| 43 | +The use of complex types for arguments in Go means a few different |
| 44 | +things for us, it provides us with the equivalent of named parameters |
| 45 | +from other languages, and it reduces the headache of figuring out which |
| 46 | +argument is which that often occurs with three or more arguments. |
35 | 47 |
|
36 |
| -If you want to let other parts of the application react to changes in a service, you can publish your own events: |
| 48 | +On the flip-side, it means that all input parameters are optional and |
| 49 | +that it is up to the programmer to make sure that the zero value is |
| 50 | +useful or at least safe for all fields and that while it's easy to add |
| 51 | +another field, if that field must be set for the correct function of the |
| 52 | +service that is not detectable at compile time. |
37 | 53 |
|
38 |
| -```go |
39 |
| -event := &events.StickersSentEvent { |
40 |
| - UserID: "taylor", |
41 |
| - Count: 1, |
42 |
| -} |
43 |
| -if err := s.bus.Publish(event); err != nil { |
44 |
| - return err |
45 |
| -} |
46 |
| -``` |
| 54 | +### Queries with Result fields |
47 | 55 |
|
48 |
| -## Commands |
| 56 | +Some queries have a Result field that is mutated and populated by the |
| 57 | +method being called. This is a remainder from when the _bus_ was used |
| 58 | +for sending commands and queries as well as for events. |
49 | 59 |
|
50 |
| -A command is a request for an action to be taken. Unlike an event's fire-and-forget approach, a command can fail as it is handled. The handler will then return an error. |
| 60 | +All bus commands and queries had to implement the Go type |
| 61 | +`func(ctx context.Context, msg interface{}) error` |
| 62 | +and mutation of the `msg` variable or returning structured information in |
| 63 | +`error` were the two most convenient ways to communicate with the caller. |
51 | 64 |
|
52 |
| -> Because we request an operation to be performed, command are written in imperative mood, such as `CreateFolderCommand`, and `DeletePlaylistCommand`. |
| 65 | +All `Result` fields should be refactored so that they are returned from |
| 66 | +the query method: |
53 | 67 |
|
54 |
| -### Dispatch a command |
55 |
| - |
56 |
| -To dispatch a command, pass the `context.Context` and object to the `DispatchCtx` method: |
| 68 | +``` |
| 69 | +type GetQuery struct { |
| 70 | + Something int |
57 | 71 |
|
58 |
| -```go |
59 |
| -// context.Context from caller |
60 |
| -ctx := req.Request.Context() |
61 |
| -cmd := &models.SendStickersCommand { |
62 |
| - UserID: "taylor", |
63 |
| - Count: 1, |
| 72 | + Result ResultType |
64 | 73 | }
|
65 |
| -if err := s.bus.DispatchCtx(ctx, cmd); err != nil { |
66 |
| - if err == bus.ErrHandlerNotFound { |
67 |
| - return nil |
68 |
| - } |
69 |
| - return err |
| 74 | +
|
| 75 | +func (s *Service) Get(ctx context.Context, cmd *GetQuery) error { |
| 76 | + // ...do something |
| 77 | + cmd.Result = result |
| 78 | + return nil |
70 | 79 | }
|
71 | 80 | ```
|
72 | 81 |
|
73 |
| -> **Note:** `DispatchCtx` will return an error if no handler is registered for that command. |
| 82 | +should become |
74 | 83 |
|
75 |
| -> **Note:** `Dispatch` currently exists and requires no `context.Context` to be provided, but it's strongly suggested to not use this since there's an ongoing refactoring to remove usage of non-context-aware functions/methods and use context.Context everywhere. |
76 |
| -
|
77 |
| -**Tip:** Browse the available commands in the `models` package. |
78 |
| - |
79 |
| -### Handle commands |
80 |
| - |
81 |
| -Let other parts of the application dispatch commands to a service, by registering a _command handler_: |
82 |
| - |
83 |
| -To handle a command, register a command handler in the `Init` function. |
84 |
| - |
85 |
| -```go |
86 |
| -func (s *MyService) Init() error { |
87 |
| - s.bus.AddHandlerCtx(s.SendStickers) |
88 |
| - return nil |
| 84 | +``` |
| 85 | +type GetQuery struct { |
| 86 | + Something int |
89 | 87 | }
|
90 | 88 |
|
91 |
| -func (s *MyService) SendStickers(ctx context.Context, cmd *models.SendStickersCommand) error { |
92 |
| - // ... |
| 89 | +func (s *Service) Get(ctx context.Context, cmd GetQuery) (ResultType, error) { |
| 90 | + // ...do something |
| 91 | + return result, nil |
93 | 92 | }
|
94 | 93 | ```
|
95 | 94 |
|
96 |
| -> **Note:** The handler method may return an error if unable to complete the command. |
| 95 | +## Events |
97 | 96 |
|
98 |
| -> **Note:** `AddHandler` currently exists and requires no `context.Context` to be provided, but it's strongly suggested to not use this since there's an ongoing refactoring to remove usage of non-context-aware functions/methods and use context.Context everywhere. |
| 97 | +An event is something that happened in the past. Since an event has already happened, you can't change it. Instead, you can react to events by triggering additional application logic to be run, whenever they occur. |
99 | 98 |
|
100 |
| -## Queries |
| 99 | +> Because they happened in the past, event names are written in past tense, such as `UserCreated`, and `OrgUpdated`. |
101 | 100 |
|
102 |
| -A command handler can optionally populate the command sent to it. This pattern is commonly used to implement _queries_. |
| 101 | +### Subscribe to an event |
103 | 102 |
|
104 |
| -### Making a query |
| 103 | +In order to react to an event, you first need to _subscribe_ to it. |
105 | 104 |
|
106 |
| -To make a query, dispatch the query instance just like you would a command. When the `DispatchCtx` method returns, the `Results` field contains the result of the query. |
| 105 | +To subscribe to an event, register an _event listener_ in the service's `Init` method: |
107 | 106 |
|
108 | 107 | ```go
|
109 |
| -// context.Context from caller |
110 |
| -ctx := req.Request.Context() |
111 |
| -query := &models.FindDashboardQuery{ |
112 |
| - ID: "foo", |
113 |
| -} |
114 |
| -if err := bus.Dispatch(ctx, query); err != nil { |
115 |
| - return err |
| 108 | +func (s *MyService) Init() error { |
| 109 | + s.bus.AddEventListener(s.UserCreated) |
| 110 | + return nil |
116 | 111 | }
|
117 |
| -// The query now contains a result. |
118 |
| -for _, item := range query.Results { |
| 112 | + |
| 113 | +func (s *MyService) UserCreated(event *events.UserCreated) error { |
119 | 114 | // ...
|
120 | 115 | }
|
121 | 116 | ```
|
122 | 117 |
|
123 |
| -> **Note:** `Dispatch` currently exists and requires no `context.Context` to be provided, but it's strongly suggested to not use this since there's an ongoing refactoring to remove usage of non-context-aware functions/methods and use context.Context everywhere. |
| 118 | +**Tip:** Browse the available events in the `events` package. |
124 | 119 |
|
125 |
| -### Return query results |
| 120 | +### Publish an event |
126 | 121 |
|
127 |
| -To return results for a query, set any of the fields on the query argument before returning: |
| 122 | +If you want to let other parts of the application react to changes in a service, you can publish your own events: |
128 | 123 |
|
129 | 124 | ```go
|
130 |
| -func (s *MyService) FindDashboard(ctx context.Context, query *models.FindDashboardQuery) error { |
131 |
| - // ... |
132 |
| - query.Result = dashboard |
133 |
| - return nil |
| 125 | +event := &events.StickersSentEvent { |
| 126 | + UserID: "taylor", |
| 127 | + Count: 1, |
| 128 | +} |
| 129 | +if err := s.bus.Publish(event); err != nil { |
| 130 | + return err |
134 | 131 | }
|
135 | 132 | ```
|
0 commit comments