Docs: Update backend architecture contributor documentation (#51172)

Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
This commit is contained in:
Emil Tullstedt
2022-06-30 10:47:10 -04:00
committed by GitHub
co-authored by Sofia Papagiannaki
parent d63ffa314e
commit 0e7a495829
3 changed files with 380 additions and 101 deletions
@@ -2,9 +2,95 @@
Grafana uses a _bus_ to pass messages between different parts of the application. All communication over the bus happens synchronously.
> **Deprecated:** The bus has officially been deprecated, however, we're still using the command/query objects paradigms.
## Commands and queries
There are three types of messages: _events_, _commands_, and _queries_.
Grafana structures arguments to [services](services.md) using a command/query
separation where commands are instructions for a mutation and queries retrieve
records from a service.
Services should define their methods as `func[T, U any](ctx context.Context, args T) (U, error)`.
Each function should take two arguments. First, a `context.Context` that
carries information about the tracing span, cancellation, and similar
runtime information that might be relevant to the call. Secondly, `T` is
a `struct` defined in the service's root package (see the instructions
for [package hierarchy](package-hierarchy.md)) that contains zero or
more arguments that can be passed to the method.
The return values is more flexible, and may consist of none, one, or two
values. If there are two values returned, the second value should be
either an `bool` or `error` indicating the success or failure of the
call. The first value `U` carries a value of any exported type that
makes sense for the service.
Following is an example of an interface providing method signatures for
some calls adhering to these guidelines:
```
type Alphabetical interface {
// GetLetter returns either an error or letter.
GetLetter(context.Context, GetLetterQuery) (Letter, error)
// ListCachedLetters cannot fail, and doesn't return an error.
ListCachedLetters(context.Context, ListCachedLettersQuery) Letters
// DeleteLetter doesn't have any return values other than errors, so it
// returns only an error.
DeleteLetter(context.Contxt, DeleteLetterCommand) error
}
```
> Because we request an operation to be performed, command are written in imperative mood, such as `CreateFolderCommand`, `GetDashboardQuery` and `DeletePlaylistCommand`.
The use of complex types for arguments in Go means a few different
things for us, it provides us with the equivalent of named parameters
from other languages, and it reduces the headache of figuring out which
argument is which that often occurs with three or more arguments.
On the flip-side, it means that all input parameters are optional and
that it is up to the programmer to make sure that the zero value is
useful or at least safe for all fields and that while it's easy to add
another field, if that field must be set for the correct function of the
service that is not detectable at compile time.
### Queries with Result fields
Some queries have a Result field that is mutated and populated by the
method being called. This is a remainder from when the _bus_ was used
for sending commands and queries as well as for events.
All bus commands and queries had to implement the Go type
`func(ctx context.Context, msg interface{}) error`
and mutation of the `msg` variable or returning structured information in
`error` were the two most convenient ways to communicate with the caller.
All `Result` fields should be refactored so that they are returned from
the query method:
```
type GetQuery struct {
Something int
Result ResultType
}
func (s *Service) Get(ctx context.Context, cmd *GetQuery) error {
// ...do something
cmd.Result = result
return nil
}
```
should become
```
type GetQuery struct {
Something int
}
func (s *Service) Get(ctx context.Context, cmd GetQuery) (ResultType, error) {
// ...do something
return result, nil
}
```
## Events
@@ -44,92 +130,3 @@ if err := s.bus.Publish(event); err != nil {
return err
}
```
## Commands
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.
> Because we request an operation to be performed, command are written in imperative mood, such as `CreateFolderCommand`, and `DeletePlaylistCommand`.
### Dispatch a command
To dispatch a command, pass the `context.Context` and object to the `DispatchCtx` method:
```go
// context.Context from caller
ctx := req.Request.Context()
cmd := &models.SendStickersCommand {
UserID: "taylor",
Count: 1,
}
if err := s.bus.DispatchCtx(ctx, cmd); err != nil {
if err == bus.ErrHandlerNotFound {
return nil
}
return err
}
```
> **Note:** `DispatchCtx` will return an error if no handler is registered for that command.
> **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.
**Tip:** Browse the available commands in the `models` package.
### Handle commands
Let other parts of the application dispatch commands to a service, by registering a _command handler_:
To handle a command, register a command handler in the `Init` function.
```go
func (s *MyService) Init() error {
s.bus.AddHandlerCtx(s.SendStickers)
return nil
}
func (s *MyService) SendStickers(ctx context.Context, cmd *models.SendStickersCommand) error {
// ...
}
```
> **Note:** The handler method may return an error if unable to complete the command.
> **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.
## Queries
A command handler can optionally populate the command sent to it. This pattern is commonly used to implement _queries_.
### Making a query
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.
```go
// context.Context from caller
ctx := req.Request.Context()
query := &models.FindDashboardQuery{
ID: "foo",
}
if err := bus.Dispatch(ctx, query); err != nil {
return err
}
// The query now contains a result.
for _, item := range query.Results {
// ...
}
```
> **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.
### Return query results
To return results for a query, set any of the fields on the query argument before returning:
```go
func (s *MyService) FindDashboard(ctx context.Context, query *models.FindDashboardQuery) error {
// ...
query.Result = dashboard
return nil
}
```