mirror of
https://github.com/grafana/grafana.git
synced 2026-08-18 17:15:08 -05:00
Docs: Update backend architecture contributor documentation (#51172)
Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
This commit is contained in:
co-authored by
Sofia Papagiannaki
parent
d63ffa314e
commit
0e7a495829
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user