Skip to content

Commit

Permalink
Docs: Update backend architecture contributor documentation (grafana#…
Browse files Browse the repository at this point in the history
…51172)

Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
  • Loading branch information
sakjur and papagian authored Jun 30, 2022
1 parent d63ffa3 commit 0e7a495
Show file tree
Hide file tree
Showing 3 changed files with 375 additions and 96 deletions.
169 changes: 83 additions & 86 deletions contribute/architecture/backend/communication.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,134 +2,131 @@

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.

## Events
Services should define their methods as `func[T, U any](ctx context.Context, args T) (U, error)`.

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.
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.

> Because they happened in the past, event names are written in past tense, such as `UserCreated`, and `OrgUpdated`.
### Subscribe to an event

In order to react to an event, you first need to _subscribe_ to it.

To subscribe to an event, register an _event listener_ in the service's `Init` 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.

```go
func (s *MyService) Init() error {
s.bus.AddEventListener(s.UserCreated)
return nil
}
Following is an example of an interface providing method signatures for
some calls adhering to these guidelines:

func (s *MyService) UserCreated(event *events.UserCreated) error {
// ...
```
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
}
```

**Tip:** Browse the available events in the `events` package.
> Because we request an operation to be performed, command are written in imperative mood, such as `CreateFolderCommand`, `GetDashboardQuery` and `DeletePlaylistCommand`.
### Publish an event
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.

If you want to let other parts of the application react to changes in a service, you can publish your own events:
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.

```go
event := &events.StickersSentEvent {
UserID: "taylor",
Count: 1,
}
if err := s.bus.Publish(event); err != nil {
return err
}
```
### Queries with Result fields

## Commands
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.

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.
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.

> Because we request an operation to be performed, command are written in imperative mood, such as `CreateFolderCommand`, and `DeletePlaylistCommand`.
All `Result` fields should be refactored so that they are returned from
the query method:

### Dispatch a command

To dispatch a command, pass the `context.Context` and object to the `DispatchCtx` method:
```
type GetQuery struct {
Something int
```go
// context.Context from caller
ctx := req.Request.Context()
cmd := &models.SendStickersCommand {
UserID: "taylor",
Count: 1,
Result ResultType
}
if err := s.bus.DispatchCtx(ctx, cmd); err != nil {
if err == bus.ErrHandlerNotFound {
return nil
}
return err
func (s *Service) Get(ctx context.Context, cmd *GetQuery) error {
// ...do something
cmd.Result = result
return nil
}
```

> **Note:** `DispatchCtx` will return an error if no handler is registered for that command.
should become

> **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
```
type GetQuery struct {
Something int
}
func (s *MyService) SendStickers(ctx context.Context, cmd *models.SendStickersCommand) error {
// ...
func (s *Service) Get(ctx context.Context, cmd GetQuery) (ResultType, error) {
// ...do something
return result, nil
}
```

> **Note:** The handler method may return an error if unable to complete the command.
## Events

> **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.
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.

## Queries
> Because they happened in the past, event names are written in past tense, such as `UserCreated`, and `OrgUpdated`.
A command handler can optionally populate the command sent to it. This pattern is commonly used to implement _queries_.
### Subscribe to an event

### Making a query
In order to react to an event, you first need to _subscribe_ to it.

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.
To subscribe to an event, register an _event listener_ in the service's `Init` method:

```go
// context.Context from caller
ctx := req.Request.Context()
query := &models.FindDashboardQuery{
ID: "foo",
}
if err := bus.Dispatch(ctx, query); err != nil {
return err
func (s *MyService) Init() error {
s.bus.AddEventListener(s.UserCreated)
return nil
}
// The query now contains a result.
for _, item := range query.Results {

func (s *MyService) UserCreated(event *events.UserCreated) error {
// ...
}
```

> **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 events in the `events` package.

### Return query results
### Publish an event

To return results for a query, set any of the fields on the query argument before returning:
If you want to let other parts of the application react to changes in a service, you can publish your own events:

```go
func (s *MyService) FindDashboard(ctx context.Context, query *models.FindDashboardQuery) error {
// ...
query.Result = dashboard
return nil
event := &events.StickersSentEvent {
UserID: "taylor",
Count: 1,
}
if err := s.bus.Publish(event); err != nil {
return err
}
```
81 changes: 81 additions & 0 deletions contribute/architecture/backend/errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Errors

Grafana introduced its own error type `github.com/grafana/grafana/pkg/util/errutil.Error`
in June 2022. It's built on top of the Go `error` interface extended to
contain all the information necessary by Grafana to handle errors in an
informative and safe way.

Previously, Grafana has passed around regular Go errors and have had to
rely on bespoke solutions in API handlers to communicate informative
messages to the end-user. With the new `errutil.Error`, the API handlers
can be slimmed as information about public messaging, structured data
related to the error, localization metadata, log level, HTTP status
code, and so forth are carried by the error.

## Basic use

### Declaring errors

For a service, declare the different categories of errors that may occur
from your service (this corresponds to what you might want to have
specific public error messages or their templates for) by globally
constructing variables using the `errutil.NewBase(status, messageID, opts...)`
function.

The status code loosely corresponds to HTTP status codes and provides a
default log level for errors to ensure that the request logging is
properly informing administrators about various errors occurring in
Grafana (e.g. `StatusBadRequest` is generally speaking not as relevant
as `StatusInternal`). All available status codes live in the `errutil`
package and have names starting with `Status`.

The messageID is constructed as `<servicename>.<error-identifier>` where
the `<servicename>` corresponds to the root service directory per
[the package hierarchy](package-hierarchy.md) and `<error-identifier>`
is a short identifier using dashes for word separation that identifies
the specific category of errors within the service.

To set a static message sent to the client when the error occurs, the
`errutil.WithPublicMessage(message string)` option may be appended to
the NewBase function call. For dynamic messages or more options, refer
to the `errutil` package's GoDocs.

Errors are then constructed using the `Base.Errorf` method, which
functions like the [fmt.Errorf](https://pkg.go.dev/fmt#Errorf) method
except that it creates an `errutil.Error`.

```go
package main

import (
"errors"
"github.com/grafana/grafana/pkg/util/errutil"
"example.org/thing"
)

var ErrBaseNotFound = errutil.NewBase(errutil.StatusNotFound, "main.not-found", errutil.WithPublicMessage("Thing not found"))

func Look(id int) (*Thing, error) {
t, err := thing.GetByID(id)
if errors.Is(err, thing.ErrNotFound) {
return nil, ErrBaseNotFound.Errorf("did not find thing with ID %d: %w", id, err)
}

return t, nil
}
```

Check out [errutil's GoDocs](https://pkg.go.dev/github.com/grafana/grafana@v0.0.0-20220621133844-0f4fc1290421/pkg/util/errutil)
for details on how to construct and use Grafana style errors.

### Handling errors in the API

API handlers use the `github.com/grafana/grafana/pkg/api/response.Err`
function to create responses based on `errutil.Error`s.

> **Note:** (@sakjur 2022-06) `response.Err` requires all errors to be
> `errutil.Error` or it'll be considered an internal server error.
> This is something that should be fixed in the near future to allow
> fallback behavior to make it possible to correctly handle Grafana
> style errors if they're present but allow fallback to a reasonable
> default otherwise.
Loading

0 comments on commit 0e7a495

Please sign in to comment.