snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
BIN
docs/develop/contribute/more-info/server/ESR-diagram.png
Normal file
BIN
docs/develop/contribute/more-info/server/ESR-diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
100
docs/develop/contribute/more-info/server/cli-commands.md
Normal file
100
docs/develop/contribute/more-info/server/cli-commands.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: "CLI commands"
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
As of 6.0, Mattermost CLI has been replaced by [mmctl](https://github.com/mattermost/mmctl). `mmctl` is built to enable access to Mattermost server from the command line. The tool leverages the public API so that administrator and user tasks can be performed.
|
||||
|
||||
Since `mmctl` uses the public API, an authorization mechanism is required. Which means the access rights are managed on the server side. There is a pre-run check to read credentials and use it in the client. In addition to authentication via credentials, `mmctl` can communicate to a local server without any authentication. This must be enabled via server configuration and both `mmctl` and `mattermost/server` needs to be running in the same machine.
|
||||
|
||||
In addition to provide more functionality towards testing and development, `db` subcommand has been added Mattermost server binary.
|
||||
|
||||
The CLI interface is written using [Cobra](https://github.com/spf13/cobra), a
|
||||
powerful and modern CLI creation library. If you have never used Cobra before, it is
|
||||
well documented in its [GitHub Repository](https://github.com/spf13/cobra).
|
||||
|
||||
The source code used to build our CLI interface is written in the `commands` directory of the [mmctl](https://github.com/mattermost/mmctl) repository.
|
||||
|
||||
Each "command" of the CLI is stored in a different file of the
|
||||
`commands` directory. Within each file, you can find
|
||||
multiple "subcommands".
|
||||
|
||||
## Add a new subcommand
|
||||
|
||||
If you want to add a new subcommand in an existing mattermost command, first find the relevant file. For example, if you want to add a `show` command to
|
||||
the `channel` command, go to `commands/channel.go` and add your subcommand there.
|
||||
|
||||
To add the subcommand, start by creating a new `Command` instance, for example:
|
||||
|
||||
```go
|
||||
var ChannelShowCmd = &cobra.Command{
|
||||
Use: "show",
|
||||
Short: "Show channel info",
|
||||
Long: "Show channel information, including the name, header, purpose and the number of members.",
|
||||
Example: " channel show --team myteam --channel mychannel"
|
||||
RunE: showChannelCmdF,
|
||||
}
|
||||
```
|
||||
|
||||
Then implement the subcommand function, in this example `showChannelCmdF`.
|
||||
|
||||
```go
|
||||
func showChannelCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
||||
// Your code implementing the command itself
|
||||
newChannel, _, err := c.ShowChannel(channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Now, you set the flags of your subcommand and register it in the command. In our case we register our new `ChannelShowCmd` flag in `ChannelCmd`.
|
||||
|
||||
```go
|
||||
func init() {
|
||||
...
|
||||
|
||||
ChannelShowCmd.Flags().String("team", "", "Team name or ID")
|
||||
ChannelShowCmd.Flags().String("channel", "", "Channel name or ID")
|
||||
...
|
||||
ChannelCmd.AddCommand(
|
||||
...
|
||||
ChannelShowCmd,
|
||||
)
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Finally, implement unit tests in `commands/channel_test.go` and end-to-end tests to commands/channel_e2e_test.go`.
|
||||
|
||||
## Add a new command
|
||||
|
||||
If you want to add a new command to `mmctl`, first create a file for the command.
|
||||
For example, if you want to add a new `emoji` command to manage emojis in
|
||||
Mattermost from the CLI, create `commands/emoji.go`
|
||||
and add your command and your subcommands there.
|
||||
|
||||
A command is exactly the same as a subcommand, so you can follow the same
|
||||
steps of the previous section. However, you must also register the new command in the
|
||||
"Root" command as follows:
|
||||
|
||||
```go
|
||||
var EmojiCmd = &cobra.Command{
|
||||
Use: "emoji",
|
||||
Short: "Emoji management",
|
||||
Long: "Lists, creates and deletes custom emoji",
|
||||
}
|
||||
func init() {
|
||||
...
|
||||
RootCmd.AddCommand(EmojiCmd)
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Usually, you would then add several subcommands to perform various tasks.
|
||||
|
||||
## Submit your pull request
|
||||
|
||||
Please submit a pull request against the [mattermost/mmctl](https://github.com/mattermost/mmctl) repository by [following these instructions](/developers/contribute/more-info/server/developer-workflow).
|
||||
31
docs/develop/contribute/more-info/server/dependencies.md
Normal file
31
docs/develop/contribute/more-info/server/dependencies.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: "Dependencies"
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
|
||||
The Mattermost server uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
|
||||
|
||||
## Add or update a new dependency
|
||||
|
||||
Adding a dependency is easy. All you have to do is import the dependency in the code and recompile. The dependency will be automatically added for you. Updating uses the same procedure.
|
||||
|
||||
Before committing the code with your new dependency added, be sure to run `go mod tidy` to maintain a consistent format and `go mod vendor` to synchronize the vendor directory.
|
||||
|
||||
If you want to add or update to a specific version of a dependency you can use a command of the form:
|
||||
```bash
|
||||
go get -u github.com/pkg/errors@v0.8.1
|
||||
go mod tidy
|
||||
go mod vendor
|
||||
```
|
||||
|
||||
If you just want whatever the latest version is, you can leave off the `@version` tag.
|
||||
|
||||
## Remove a dependency
|
||||
|
||||
Be sure you have enabled go modules support. After removing all references to the dependency in the code, you run:
|
||||
```bash
|
||||
go mod tidy
|
||||
go mod vendor
|
||||
```
|
||||
to remove it from the `go.mod` file and the `vendor` directory.
|
||||
113
docs/develop/contribute/more-info/server/developer-workflow.md
Normal file
113
docs/develop/contribute/more-info/server/developer-workflow.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: "Server workflow"
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
If you haven't [set up your developer environment](/developers/contribute/developer-setup), please do so before continuing with this section.
|
||||
|
||||
Join the [Developers community channel](https://community.mattermost.com/core/channels/developers) to ask questions from community members and the Mattermost core team.
|
||||
|
||||
### Workflow
|
||||
|
||||
Here's a general workflow for a Mattermost developer working on the [mattermost](https://github.com/mattermost/mattermost) repository:
|
||||
|
||||
#### Making code changes
|
||||
1. Review the repository structure to familiarize yourself with the project:
|
||||
|
||||
* [./server/channels/api4/](https://github.com/mattermost/mattermost/tree/master/server/channels/api4) holds all API and application related code.
|
||||
* [./server/public/model/](https://github.com/mattermost/mattermost/tree/master/server/public/model) holds all data model definitions and the Go driver.
|
||||
* [./server/channels/store/](https://github.com/mattermost/mattermost/tree/master/server/channels/store) holds all database querying code.
|
||||
* [./server/channels/utils/](https://github.com/mattermost/mattermost/tree/master/server/channels/utils) holds all utilities, such as the mail utility.
|
||||
* [./server/i18n/](https://github.com/mattermost/mattermost/tree/master/server/i18n) holds all localization files for the server.
|
||||
2. On your fork, create a feature branch for your changes. Name it `MM-$NUMBER_$DESCRIPTION` where `$NUMBER` is the [Jira](https://mattermost.atlassian.net) ticket number you are working on and `$DESCRIPTION` is a short description of your changes. Example branch names are `MM-18150_plugin-panic-log` and `MM-22037_uppercase-email`.
|
||||
3. Make the code changes required to complete your ticket.
|
||||
#### Running and writing tests
|
||||
4. Ensure that unit tests are written or modified where appropriate. For the server repository in general, Mattermost follows the opinionated way of testing in Go. You can learn more about this process in [DigitalOcean's How To Write Unit Tests in Go tutorial](https://www.digitalocean.com/community/tutorials/how-to-write-unit-tests-in-go-using-go-test-and-the-testing-package). Test files must always end with `_test.go`, and should be located in the same folder where the code they are checking lives. For example, check out [download.go](https://github.com/mattermost/mattermost/blob/master/server/channels/app/download.go) and [download_test.go](https://github.com/mattermost/mattermost/blob/master/server/channels/app/download_test.go), which are both located in the `app` folder. Please also use [testify](https://github.com/stretchr/testify) for new tests.
|
||||
5. If you made changes to the store, run `make store-mocks` and `make store-layers` to update test mocks and timing layer.
|
||||
6. To test your changes, run `make run-server` from the root directory of the server repository. This will start up the server at `http://localhost:8065`. To get changes to the server it must be restarted with `make restart-server`. If you want to test with the web app, you may also run `make run` which will start the server and a watcher for changes to the web app.
|
||||
7. Once everything works to meet the ticket requirements, stop Mattermost by running `make stop` in the server repository, then run `make check-style` to check your syntax.
|
||||
8. Run the tests using one or more of the following options:
|
||||
* Run `make test` to run all the tests in the project. This may take a long time and provides very little feedback while it's running.
|
||||
* Run individual tests by name executing `go test -run "TestName" ./<directory>`.
|
||||
* Run all the tests in a package where changes were made executing `go test app`.
|
||||
* Create a draft PR with your changes and let our CI servers run the tests for you.
|
||||
9. Running every single unit test takes a lot of time while making changes, so you can run a subset of the server-side unit tests by using the following:
|
||||
```
|
||||
go test -v -run='<test name or regex>' ./<package containing test>
|
||||
```
|
||||
For example, if you want to run `TestUpdatePost` in `app/post_test.go`, you would execute the following:
|
||||
|
||||
```
|
||||
go test -v -run='TestUpdatePost' ./app
|
||||
```
|
||||
10. If you added or changed any localization strings you will need to run `make i18n-extract` to generate the new/updated strings.
|
||||
#### Testing email notifications
|
||||
|
||||
11. When Docker starts, the SMTP server is available on port 2500. A username and password are not required. You can access the Inbucket webmail on port 9000. For additional information on configuring an SMTP email server, including troubleshooting steps, see the [SMTP email setup page in the Mattermost user documentation](https://docs.mattermost.com/install/smtp-email-setup.html).
|
||||
#### Testing with GitLab Omnibus
|
||||
|
||||
12. To test a locally compiled version of Mattermost with GitLab Omnibus, replace the following GitLab files:
|
||||
* The compiled `mattermost` binary in `/opt/gitlab/embedded/bin/mattermost`.
|
||||
* The assets (templates, i18n, fonts, webapp) in `/opt/gitlab/embedded/service/mattermost`.
|
||||
#### Creating a pull request (PR)
|
||||
13. Commit your changes, push your branch, and [create a pull request](/developers/contribute/more-info/getting-started/contribution-checklist).
|
||||
14. Once a PR is submitted it's best practice to avoid rebasing on the base branch or force-pushing. Jesse, a developer at Mattermost, mentions this in his blog article [Submitting Great PRs](https://mattermost.com/blog/submitting-great-prs/). When the PR is merged, all the PR's commits are automatically squashed into one commit, so you don't need to worry about having multiple commits on the PR.
|
||||
15. That's it! Rejoice that you've helped make Mattermost better.
|
||||
|
||||
### Useful Server makefile commands
|
||||
|
||||
Some useful `make` commands include:
|
||||
|
||||
* `make run` runs the server, creates a symlink for your mattermost-webapp folder, and starts a watcher for the web app.
|
||||
* `make stop` stops the server and the web app watcher.
|
||||
* `make run-server` runs only the server and not the client.
|
||||
* `make debug-server` will run the server in the `delve` debugger.
|
||||
* `make stop-server` stops only the server.
|
||||
* `make update-docker` stops and updates your Docker images. This is needed if any changes are made to `docker-compose.yaml`.
|
||||
* `make clean-docker` stops and removes your Docker images and is a good way to wipe your database.
|
||||
* `make clean` cleans your local environment of temporary files.
|
||||
* `make config-reset` resets the `config/config.json` file to the default.
|
||||
* `make nuke` wipes your local environment back to a completely fresh start.
|
||||
* `make package` creates packages for distributing your builds and puts them in the `./dist` directory. You will first need to run `make build` and `make build-client`.
|
||||
|
||||
If you would like to run the development environment without Docker you can set the `MM_NO_DOCKER` environment variable. If you do this, you will need to set up your own database and any of the other services needed to run Mattermost.
|
||||
|
||||
### Useful Mattermost and mmctl commands
|
||||
|
||||
During development you may want to reset the database and generate random data for testing your changes. For this purpose, Mattermost has the following commands in the Mattermost CLI:
|
||||
|
||||
1. First, install the server with `go install ./cmd/mattermost` in the server repository.
|
||||
2. You can reset your database to the initial state using:
|
||||
|
||||
```
|
||||
mattermost db reset
|
||||
```
|
||||
3. The following commands need to be run via the [mmctl](https://docs.mattermost.com/manage/mmctl-command-line-tool.html) tool.
|
||||
|
||||
* You can generate random data to populate the Mattermost database using:
|
||||
|
||||
```
|
||||
mmctl sampledata
|
||||
```
|
||||
|
||||
* Create an account using the following command:
|
||||
|
||||
```
|
||||
mmctl user create --email user@example.com --username test1 --password mypassword
|
||||
```
|
||||
|
||||
* Optionally, you can assign that account System Admin rights with the following command:
|
||||
|
||||
```
|
||||
mmctl user create --email user@example.com --username test1 --password mypassword --system-admin
|
||||
```
|
||||
|
||||
### Customize your workflow
|
||||
|
||||
#### Makefile variables
|
||||
|
||||
You can customize variables of the Makefile by creating a `config.override.mk` file or setting environment variables. To get started, you can copy the `config.mk` file to `config.override.mk` and change the values in your newly copied file.
|
||||
|
||||
#### Docker-compose configurations
|
||||
|
||||
If you create a `docker-compose.override.yaml` file at the root of the project, it will be automatically loaded by all the `Makefile` tasks using `docker-compose`, allowing you to define your own services or change the configuration of the ones Mattermost provides.
|
||||
122
docs/develop/contribute/more-info/server/feature-flags.md
Normal file
122
docs/develop/contribute/more-info/server/feature-flags.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: "Feature flags"
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# What are feature flags
|
||||
|
||||
Feature flag is a software development technique that turns functionality on and off without deploying new code. Feature flags allow us to be more confident in shipping features continuously to Mattermost Cloud. Feature flags also allow us to control which features are enabled on a cluster level.
|
||||
|
||||
# How to use feature flags
|
||||
|
||||
## When to use
|
||||
|
||||
There are no hard rules on when a feature flag should be used. It is left up to the best judgement of the responsible engineers to determine if a feature flag is required. The following are guidelines designed to help the determination:
|
||||
|
||||
- Any "substantial" feature should have a flag
|
||||
- Features that are probably substantial:
|
||||
- Features with new UI or changes to existing UI
|
||||
- Features with a risk of regression
|
||||
- Features that are probably not substantial:
|
||||
- Small bug fixes
|
||||
- Refactoring
|
||||
- Changes that are not user facing and can be completely verified by unit and E2E testing.
|
||||
|
||||
In all cases, ask yourself: Why do I need to add a feature flag? If I don't add one, what options do I have to control the impact on user experience (e.g. a config setting or System Console setting)?
|
||||
|
||||
## Add the feature flag in code
|
||||
|
||||
1. Add the new flag to the feature flag struct located in `model/feature_flags.go`.
|
||||
2. Set a default value in the `SetDefaults` function in the same file.
|
||||
3. Use the feature flag in code as you would use a regular configuration setting. In tests, manipulate the configuration value to test value changes, such as activation and deactivation of the feature flag.
|
||||
4. Code may be merged regardless of setup in the management system. In this case it will always take the default value supplied in the `SetDefaults` function.
|
||||
5. Create a removal ticket for the feature flag. All feature flags should be removed as soon as they have been verified by Cloud. The ticket should encompass removal of the supporting code and archiving in the management system.
|
||||
|
||||
### Feature flag code guidelines
|
||||
|
||||
- A ticket should be created when a feature flag is added to remove the feature flag as soon as it isn't required anymore.
|
||||
- Tests should be written to verify the feature flag works as expected. Note that in cases where there may be a migration or new data, off to on and on to off should both be tested.
|
||||
- Log messages by the feature should include the feature flag tag, with the feature flag name as a value, in order to ease debugging.
|
||||
|
||||
# Changing Feature Flag Values
|
||||
|
||||
## Self Hosted (and local development)
|
||||
|
||||
Feature flag values can be changed via environment variables. The environment variable set follows the pattern `MM_FEATUREFLAGS_<name>` where `<name>` is the uppercase key of the feature flag you added to model/feature_flags.go
|
||||
|
||||
## Cloud
|
||||
|
||||
Feature flag adjustments (ie, turning something on or off) in the Mattermost Cloud environment are owned and controlled by the Cloud team. To change the value for a feature flag, please open a ticket.
|
||||
|
||||
## Timelines for rollouts
|
||||
|
||||
Typically feature flag will initially disable the feature. It's a good idea to test the feature during a safe time or on a subset of instances. Each team can decide what's best and there's no need to request the flag value changes from the Cloud team. If you think there might be a performance impact there's no harm in communicating your plan beforehand.
|
||||
|
||||
|
||||
<Note title="Note">
|
||||
The steps below are an initial guideline and will be iterated on over time.
|
||||
|
||||
- 1st week after feature is merged (T-30): 10% rollout; only to test servers, no rollout to customers.
|
||||
- 2nd week (T-22): 50% rollout; rollout to some customers (excluding big customers and newly signed-up customers); no major bugs in test servers.
|
||||
- 3rd week (T-15): 100% rollout; no major bugs from customers or test servers.
|
||||
- End of 3rd week (T-8): Remove flag. Feature is production ready and not experimental.
|
||||
</Note>
|
||||
|
||||
|
||||
For smaller, non-risky features, the above process can be more fast tracked as needed, such as starting with a 10% rollout to test servers, then 100%.
|
||||
Features have to soak on Cloud for at least two weeks for testing. Focus is on severity and number of bugs found; if there are major bugs found at any stage, the feature flag can be turned off to roll back the feature.
|
||||
|
||||
When the feature is rolled out to customers, logs will show if there are crashes, and normally users will report feedback on the feature (e.g. bugs).
|
||||
|
||||
## Self-hosted releases
|
||||
|
||||
For self-hosted releases, typically a flagged feature will be released in an enabled state. That said, you can release a feature to self-hosted disabled, [it's not unprecedented](https://github.com/mattermost/mattermost/blob/master/server/public/model/feature_flags.go#L75).
|
||||
|
||||
## Tests
|
||||
|
||||
Tests should be written to verify all states of the feature flag. Tests should cover any migrations that may take place in both directions (i.e., from "off" to "on" and from "on" to "off"). Ideally E2E tests should be written before the feature is merged, or at least before the feature flag is removed.
|
||||
|
||||
## Examples of feature flags
|
||||
|
||||
Some [examples are here](https://github.com/mattermost/mattermost/blob/master/server/public/model/feature_flags.go#L75).
|
||||
|
||||
## FAQ
|
||||
|
||||
1. What are the expected values for boolean feature flags?
|
||||
- Normally ``true`` or ``false``, but this may not always equate to enabled/disabled. A feature flag that introduces three new sorting algorithms can also be written:
|
||||
- "selection" (default, the existing strategy in production)
|
||||
- "bubble"
|
||||
- "quick"
|
||||
|
||||
2. Is it possible to use a plugin feature flag such as `PluginIncidentManagement` to "prepackage" a plugin only on Cloud by only setting a plugin version to that flag on Cloud? Can self-hosted customers manually set that flag to install the said plugin?
|
||||
- Yes. If you leave the default "" then nothing will happen for self-hosted installations.
|
||||
|
||||
3. How do feature flags work on webapp?
|
||||
- To add a feature flag that affects frontend, the following is needed:
|
||||
1. PR to server code to add the new feature flag.
|
||||
2. PR to redux to update the types.
|
||||
3. PR to webapp to actually use the feature flag.
|
||||
|
||||
4. How do feature flags work on mobile?
|
||||
- To add a feature flag that affects mobile, the following is needed:
|
||||
1. PR to server code to add the new feature flag.
|
||||
2. PR to mobile to update the types and to actually use the feature flag.
|
||||
|
||||
5. What is the environment variable to set a feature flag?
|
||||
- It is `MM_FEATUREFLAGS_<myflag>`.
|
||||
|
||||
6. Can plugins use feature flags to enable small features aside of the version forcing feature flag?
|
||||
- Yes. You can create feature flags as if they were added for the core product, and they'll get included in the plugin through the config.
|
||||
|
||||
|
||||
7. Do feature flag changes require the server to be restarted?
|
||||
- Feature flags don’t require a server restart unless the feature being flagged requires a restart itself.
|
||||
|
||||
8. For features that are requested by self-hosted customers, why do we have to deploy to Cloud first, rather than having the customer who has the test case test it?
|
||||
- Cloud is the way to validate the stability of the feature before it goes to self-hosted customers. In exceptional cases we can let the self-hosted customer know that they can use environment variables to enable the feature flag (but specify that the feature is experimental).
|
||||
|
||||
9. How does the current process take into account bugs that may arise on self-hosted specifically?
|
||||
- The process hasn’t changed much from the old release process: Features can still be tested on self-hosted servers once they have been rolled out to Cloud. The primary goal is that bugs are first identified on Cloud servers.
|
||||
|
||||
10. How can self-hosted installations set feature flags?
|
||||
- Self-hosted installations can set environment variables to set feature flag values. However, users should recognize that the feature is still considered "experimental" and should not be enabled on production servers.
|
||||
28
docs/develop/contribute/more-info/server/index.md
Normal file
28
docs/develop/contribute/more-info/server/index.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: "Server"
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
The server is the highly scalable backbone of the Mattermost project. Written in Go, it compiles to a single, standalone binary. It's generally stateless except for the WebSocket connections and some in-memory caches.
|
||||
|
||||
Communication with Mattermost clients and integrations mainly occurs through the RESTful JSON web API and WebSocket connections primarily used for event delivery.
|
||||
|
||||
Data storage is done with MySQL or PostgreSQL for non-binary data. Files are stored locally, on network drives or in a service such as S3 or Minio.
|
||||
|
||||
## Repository
|
||||
|
||||
https://github.com/mattermost/mattermost
|
||||
|
||||
## Server packages
|
||||
|
||||
The server consists of several different Go packages:
|
||||
|
||||
* `api4` - Version 4 of the RESTful JSON Web Service
|
||||
* `app` - Logic layer for getting, modifying, and interacting with models
|
||||
* `cmd` - Command line interface
|
||||
* `einterfaces` - Interfaces for Enterprise Edition features
|
||||
* `jobs` - Job server and scheduling
|
||||
* `model` - Definitions and helper functions for data models
|
||||
* `store` - Storage layer for interacting with caches and databases
|
||||
* `utils` - Utility functions for various tasks
|
||||
* `web` - Serves static pages
|
||||
BIN
docs/develop/contribute/more-info/server/migrations_pie.png
Normal file
BIN
docs/develop/contribute/more-info/server/migrations_pie.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
36
docs/develop/contribute/more-info/server/plugins.md
Normal file
36
docs/develop/contribute/more-info/server/plugins.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: "Plugins"
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
Mattermost supports plugins that offer powerful features for extending and deeply integrating with both the server and web/desktop apps.
|
||||
|
||||
This document covers the plugin infrastructure and how to contribute to it.
|
||||
|
||||
## Build plugins
|
||||
|
||||
Looking to build a plugin? [Then you want the plugin author documentation.](/developers/integrate/plugins)
|
||||
|
||||
## Overview
|
||||
|
||||
Plugins are generally made of at least two parts: a manifest and a server binary and/or a JavaScript bundle.
|
||||
|
||||
The manifest tells Mattermost what the plugin is and provides a set of metadata used by the server to install and run the plugin. Please see the [manifest reference](/developers/integrate/plugins/manifest-reference) for more information. Manifests may be defined in JSON or YAML.
|
||||
|
||||
The server binary is a compiled Go program that extends the [MattermostPlugin](https://godoc.org/github.com/mattermost/mattermost/server/public/plugin#MattermostPlugin) struct of the [plugin](https://godoc.org/github.com/mattermost/mattermost/server/public/plugin) package. When enabled, the plugin's server binary is started as a process by the Mattermost server. Plugin developers then have access to interact with the Mattermost server over RPC through the plugin [API](/developers/integrate/reference/server#API) and [Hooks](/developers/integrate/reference/server#Hooks). The server-side of plugins is built using the [go-plugin](https://github.com/hashicorp/go-plugin) library from Hashicorp. More information is available in the [server side of the plugin author documentation](/developers/integrate/plugins/components/server).
|
||||
|
||||
The JavaScript bundle is a webpack-built collection of JavaScript code that will be run on the Mattermost web/desktop apps. When a plugin is enabled, the client is notified and it makes a request to add the JS bundle to the document. The plugin's client code then registers itself and its components with the Mattermost client through the client's [plugin registry](/developers/integrate/reference/webapp#registry). The registry contains many methods for registering different components and callbacks. These are all stored as part of the app's [plugin reducer](https://github.com/mattermost/mattermost/blob/master/webapp/channels/src/reducers/plugins/index.ts). The [Pluggable](https://github.com/mattermost/mattermost/tree/master/webapp/channels/src/plugins/pluggable) component is then inserted into various places in the app, allowing plugins to insert components into these locations in the UI. In some special cases, the Pluggable component is not used and we instead implement the plugs manually. More information is available in the [webapp side of the plugin author documentation](/developers/integrate/plugins/components/webapp).
|
||||
|
||||
All these different components of a plugin are compressed into a .tar.gz bundle. Installing a plugin is the process of uploading this bundle to the Mattermost server (via the UI, REST API or CLI). The server then unpacks the bundle, performs some validation and extracts it into the configured directory for storing installed plugins. Installed plugins are not yet running. To start a plugin it must be enabled (again via the UI, REST API or CLI). Once it is enabled, the server will then start the server process and prepare the web app bundle for serving to the client. Plugin settings, configuration and enabled/disabled status are managed by the Mattermost `config.json` using a [PluginSettings](https://godoc.org/github.com/mattermost/mattermost/server/public/model#PluginSettings) struct.
|
||||
|
||||
Check out the [`plugin` package](https://github.com/mattermost/mattermost/tree/master/server/public/plugin) and the [plugin_* files in the `app` package](https://github.com/mattermost/mattermost/tree/master/server/channels/app) for the code, and [mattermost-plugin-demo](https://github.com/mattermost/mattermost-plugin-demo) for an example plugin. To start developing your own plugin, please follow the instructions [here](/developers/integrate/plugins/developer-setup).
|
||||
|
||||
## Add an API
|
||||
|
||||
To add a plugin API you need to add the signature of your new method to the [API interface](https://github.com/mattermost/mattermost/blob/master/server/public/plugin/api.go). You then need to implement the API in the [plugin_api.go](https://github.com/mattermost/mattermost/blob/master/server/channels/app/plugin_api.go) of the `app` package. Finally, you need to run `make pluginapi` to generate the RPC glue code needed for your new API and `make plugin-mocks` to generate the mocks used for plugin testing.
|
||||
|
||||
That's it! Submit your pull request.
|
||||
|
||||
## Questions?
|
||||
|
||||
If you have any questions, feel free to ask in the [Toolkit channel](https://community.mattermost.com/core/channels/developer-toolkit) of our Mattermost community instance.
|
||||
101
docs/develop/contribute/more-info/server/rest-api.md
Normal file
101
docs/develop/contribute/more-info/server/rest-api.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: "REST API"
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
The REST API is a JSON web service that facilitates communication between Mattermost clients, as well as integrations, and the server. The server is currently on API version 4.
|
||||
|
||||
### Reference
|
||||
|
||||
Looking for the API reference? You can find it here: [https://api.mattermost.com](https://api.mattermost.com).
|
||||
|
||||
### Add an endpoint
|
||||
|
||||
To add an endpoint to API version 4, all of the following must be completed:
|
||||
|
||||
- [Reference](#reference)
|
||||
- [Add an endpoint](#add-an-endpoint)
|
||||
- [Document the endpoint](#document-the-endpoint)
|
||||
- [Implement the API handler](#implement-the-api-handler)
|
||||
- [Update the Golang driver](#update-the-golang-driver)
|
||||
- [Write a unit test](#write-a-unit-test)
|
||||
- [Submit your pull request (PR)](#submit-your-pull-request-pr)
|
||||
- [Legacy Notes](#legacy-notes)
|
||||
|
||||
#### Document the endpoint
|
||||
At Mattermost, the [OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md) is used for API documentation. The API documentation lives in the main Mattermost repository alongside the server: [api](https://github.com/mattermost/mattermost/tree/master/api).
|
||||
|
||||
To document an endpoint, follow these steps:
|
||||
|
||||
1. Find the `.yaml` file in the [api/v4/source](https://github.com/mattermost/mattermost/tree/master/api/v4/source) directory that fits your endpoint.
|
||||
- For example, if you were adding the `GET /users/{user_id}` endpoint you would be looking for the [users.yaml](https://github.com/mattermost/mattermost/blob/master/api/v4/source/users.yaml) file.
|
||||
- If the file doesn't exist yet, you may need to create it and then update the [Makefile](https://github.com/mattermost/mattermost/tree/master/api/Makefile) to include the file.
|
||||
|
||||
2. Copy an existing endpoint from the same or a different file.
|
||||
|
||||
3. Update the documentation you copied with the correct information for your endpoint, including:
|
||||
- `Tag` - the resource type
|
||||
- `Summary` - a summary of few words
|
||||
- `Description` - a brief 1-2 sentence description
|
||||
- `Permissions` - the permission(s) required
|
||||
- `Parameters` - the URL and body parameters
|
||||
- `Responses` - the success and error responses
|
||||
|
||||
4. Confirm you don't have any syntax errors by running `make build` within the [api](https://github.com/mattermost/mattermost/tree/master/api/) directory.
|
||||
|
||||
5. Continue with the implementation of your API handler, updating this documentation as needed.
|
||||
|
||||
#### Implement the API handler
|
||||
To implement the API handler, you'll first need to [setup your developer environment](/developers/contribute/developer-setup), and then follow these steps:
|
||||
|
||||
1. Add the declaration for your endpoint. For an example, check out the [/api4/user.go](https://github.com/mattermost/mattermost/blob/master/server/channels/api4/user.go) file.
|
||||
|
||||
2. Implement the handler for your endpoint. Follow this general pattern for handlers:
|
||||
|
||||
```Go
|
||||
func handlerName(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// 1. Parse the request URL and body.
|
||||
// 2. Do a permissions check if required.
|
||||
// 3. Invoke handler logic through the app package.
|
||||
// 4. (Optional) Check the Etag.
|
||||
// 5. Format the response and write the response.
|
||||
}
|
||||
```
|
||||
For examples, see the [createUser()](https://github.com/mattermost/mattermost/blob/d693f880431741e3e1482503c4e80d6148b0f1bf/server/channels/api4/user.go#L111) and the [getUser()](https://github.com/mattermost/mattermost/blob/d693f880431741e3e1482503c4e80d6148b0f1bf/server/channels/api4/user.go#L177) handlers.
|
||||
|
||||
3. Run the server by runing `make run-server` within the [server](https://github.com/mattermost/mattermost/tree/master/server/) directory.
|
||||
|
||||
4. Use `curl` or [Postman](https://www.getpostman.com/) to test the basics of your endpoint.
|
||||
|
||||
#### Update the Golang driver
|
||||
The Go driver for APIv4 is in [/model/client4.go](https://github.com/mattermost/mattermost/blob/master/server/public/model/client4.go). To add a function to support your new endpoint:
|
||||
|
||||
1. Copy over an existing driver function, such as [CreateUser](https://github.com/mattermost/mattermost/blob/master/server/public/model/client4.go#L827).
|
||||
|
||||
2. Paste the function into the section for your endpoint. For example, `POST /teams` would go in the Teams section.
|
||||
|
||||
3. Modify the function to correctly hit your endpoint. Make sure to update the request method to match your endpoint's HTTP method.
|
||||
|
||||
#### Write a unit test
|
||||
The most important part of this process is to make sure the new endpoint works correctly. Follow these steps to write a unit test:
|
||||
|
||||
1. Open the test Go file related to your endpoint, or create one if necessary. For example, if you put your handler in [/api4/user.go](https://github.com/mattermost/mattermost/blob/master/server/channels/api4/user.go), your test will go in [/api4/user\_test.go](https://github.com/mattermost/mattermost/blob/master/server/channels/api4/user_test.go).
|
||||
|
||||
2. Write your test based on the other tests in your file (or folder). There are several helper functions in [/api4/apitestlib.go](https://github.com/mattermost/mattermost/blob/master/server/channels/api4/apitestlib.go) that you may use.
|
||||
|
||||
3. Ensure that your test covers the following:
|
||||
- All combinations of correct inputs to your endpoint.
|
||||
- Etags for your endpoint, if applicable.
|
||||
- Incorrect URL or body parameters return a **400 Bad Request** status code.
|
||||
- Requests without a token return a **401 Unauthorized** status code (for endpoints requiring a session).
|
||||
- Requests with insufficient permissions return a **403 Forbidden** status code (for endpoints requiring permission).
|
||||
- Requests to non-existent resources or URLs return a **404 Not Found** status code.
|
||||
|
||||
Returning the correct error code might require investigation in the [app](https://github.com/mattermost/mattermost/tree/master/server/channels/app) or [store](https://github.com/mattermost/mattermost/tree/master/server/channels/store) to find the source of errors. Status codes on errors should be set at the creation of the error.
|
||||
|
||||
#### Submit your pull request (PR)
|
||||
Submit your pull request against the [mattermost/mattermost](https://github.com/mattermost/mattermost) repository by [following these instructions](/developers/contribute/more-info/server/developer-workflow).
|
||||
|
||||
### Legacy Notes
|
||||
|
||||
The Mattermost API used to be defined in https://github.com/mattermost/mattermost-api-reference, but the source has since been moved to the [mattermost/mattermost](https://github.com/mattermost/mattermost) to streamline making code and documentation changes at the same time.
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
title: "DB migration guide"
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document aims to do an analysis of the types of schema migrations we do in Mattermost and ways to make them non-blocking so as to improve the Mattermost upgrade experience.
|
||||
Historically, we have never put a lot of thought to the migration process. Developers would simply add a DDL statement and call it a day. But that causes a significant impact to large customers for whom downtime is not an option. This causes them to push back on upgrading their Mattermost version for a long time (sometimes for several years). This in turn, has some cascading effects like customers not able to get new features, performance improvements etc.
|
||||
|
||||
We want to improve the situation and make upgrades a worry-free experience for our customers. It definitely comes at a cost of writing more code and delaying some features to avoid breaking changes. This document will aim to uncover all those cases and provide best practices to follow so that we can hit the right balance.
|
||||
|
||||
## Goals
|
||||
|
||||
We have two overarching goals and a third auxiliary goal.
|
||||
|
||||
1. Schema migrations should **ALWAYS** be backwards compatible until the last ESR.
|
||||
2. Schema migrations should **NEVER** lock the entire table.
|
||||
3. Reduce migration time where possible.
|
||||
|
||||
We want to strictly follow this as much as possible (even at the cost of slower feature development).
|
||||
|
||||
### Background
|
||||
|
||||
Schema changes are always made synchronously when Mattermost starts up. This means the application won't be ready to serve requests until all schema changes are applied. In most cases, the new application won't be able to work until those schema changes are in place.
|
||||
|
||||
In a high availability environment, multiple instances will try to run migrations. To prevent this, a lock table is used in the migration system. Until migrations are completed, none of the instances will start. Once the lock is released by a node, another instance will obtain the lock, and check the migrations table. Since the previous node already applied the migrations, the remaining nodes won't re-apply the migrations.
|
||||
|
||||
From Mattermost release v6.4, we have started using a schema-based migration system. We are now creating SQL statement files to run migrations. A developer must create migration files for each database driver. Since we want our migrations to be reversible, the developer must create one `up` script along with a `down` script. For instance, a single migration would have the following files:
|
||||
|
||||
- `000066_upgrade_posts_v6.0.down.sql`
|
||||
- `000066_upgrade_posts_v6.0.up.sql`
|
||||
|
||||
A file naming convention is used to determine the order in which the migrations should be applied that appends `up|down.sql` suffix to the migration name. We were using a database version before the new migration system which is why the versions exist in the migration file name in the example. Going forward, using version identifiers for future next migration files is not mandatory. A developer can add any information to the name if they think it's going to be helpful.
|
||||
|
||||
We are using [morph](https://github.com/go-morph/morph) for the migration engine. The tool has a library and also a CLI. Mattermost server imports the library to have programmatic access to morph functions. A developer can use the morph CLI tool to test whether their migrations are working properly. Please follow instructions in the morph documentation to use the morph CLI tool.
|
||||
|
||||
### Analysis
|
||||
|
||||
A rough analysis of our past schema migrations shows the following (some very early migrations were skipped which would be considered as base Mattermost):
|
||||
|
||||
```
|
||||
CREATE INDEX - 489
|
||||
ALTER TABLE - 195
|
||||
ADD COLUMN - 113
|
||||
ALTER COLUMN - 51
|
||||
DROP COLUMN - 25
|
||||
ADD CONSTRAINT - 6
|
||||
DROP INDEX - 124
|
||||
CREATE TABLE - 60
|
||||
UPDATE - 19
|
||||
DELETE - 2
|
||||
```
|
||||
|
||||

|
||||
|
||||
We will go through each of these migration types and discuss how we can make it non-blocking. This is a lengthy document, so for those wanting to directly look at the executive summary, we present it right now. And then expand on each section in detail later.
|
||||
|
||||
### Conclusions
|
||||
|
||||
|
||||
| Operation | Table rewrite | Concurrent DML allowed |
|
||||
| ----------- | ------------- | ---------------------- |
|
||||
| CREATE INDEX | NO | YES |
|
||||
| DROP INDEX | NO | YES |
|
||||
| ADD COLUMN | NO | YES<sup>1</sup> |
|
||||
| ALTER COLUMN | YES | NO |
|
||||
| DROP COLUMN | YES | YES<sup>1</sup> |
|
||||
| ADD FK CONSTRAINT| NO | YES (only selects)<sup>2</sup> |
|
||||
| ADD UNIQUE CONSTRAINT | NO | YES |
|
||||
|
||||
|
||||
|
||||
<Note title="Note">
|
||||
1. Technically it takes an ACCESS EXCLUSIVE LOCK, however it is only to add/remove the metadata. The command returns instantly.
|
||||
2. Adding FK constraint takes a SHARE ROW EXCLUSIVE [lock](https://www.postgresql.org/docs/11/sql-altertable.html).
|
||||
</Note>
|
||||
|
||||
|
||||
### Recommendations
|
||||
|
||||
- **Try to avoid FK constraints**.
|
||||
- **Strongly avoid trying to alter column types**.
|
||||
|
||||
However, if you MUST do it, take a look into the following sections.
|
||||
|
||||
### Details
|
||||
|
||||
1. CREATE INDEX
|
||||
|
||||
CREATE INDEX CONCURRENTLY does not take any locks.
|
||||
|
||||
2. ALTER TABLE ADD COLUMN
|
||||
|
||||
Adding nullable columns happens in constant time from version 10. And from version 11 onwards, adding non-null columns with a default value also happens in constant time.
|
||||
|
||||
The catch here is to be able to handle denormalization optimizations which typically adds a new column but needs to backfill that with data before using the column. Take a look at the next section on how to achieve that.
|
||||
|
||||
3. ALTER TABLE ALTER COLUMN
|
||||
|
||||
This takes an exclusive lock. We strongly recommend you avoid doing this.
|
||||
|
||||
To give some context, we have this particular migration `ALTER TABLE posts ALTER COLUMN props TYPE jsonb USING props::jsonb;` which has caused us more pain than it was worth. Several large customers have faced problems with this migration where in some cases, it has been observed to take 8+ hrs. Therefore, we strongly suggest to avoid making any `ALTER COLUMN` changes until absolutely unavoidable (for example, security issues).
|
||||
|
||||
However, if you MUST do this, then see the example later.
|
||||
|
||||
4. ALTER TABLE DROP COLUMN
|
||||
|
||||
Only a metadata lock is taken. No table rewrite takes place. The space is just marked as unused and later taken up by future DB writes.
|
||||
|
||||
5. ALTER TABLE ADD CONSTRAINT
|
||||
|
||||
Relatively rare, but out of those 6 cases, 2 are adding unique constraints. For example:
|
||||
|
||||
```sql
|
||||
ALTER TABLE oauthaccessdata ADD CONSTRAINT oauthaccessdata_clientid_userid_key UNIQUE (clientid, userid);
|
||||
```
|
||||
|
||||
This can be improved by first adding the index concurrently, and then attaching the index to the constraint. See example later.
|
||||
|
||||
Adding a foreign key in PostgreSQL takes a share row exclusive lock, which means only SELECT queries are allowed. It is possible to bypass the table scanning by adding a “NOT VALID” suffix, but then it defeats the purpose of having a foreign key. We recommend against doing it.
|
||||
|
||||
6. DROP INDEX
|
||||
|
||||
DROP INDEX CONCURRENTLY does not take any locks.
|
||||
|
||||
7. CREATE TABLE
|
||||
|
||||
Does not lock any existing data so no issues.
|
||||
|
||||
8. UPDATE
|
||||
|
||||
An analysis shows that UPDATE statements roughly fall into one of these three categories:
|
||||
|
||||
- **Data migrations**:
|
||||
|
||||
```sql
|
||||
UPDATE channelmembers SET MentionCountRoot = ..
|
||||
UPDATE Channels SET TotalMsgCountRoot = ..
|
||||
UPDATE ChannelMembers CM SET MsgCountRoot ..
|
||||
```
|
||||
|
||||
In these cases, rather than operating on the entire table, we need to operate on batches at a time. See the example later on how to achieve that.
|
||||
|
||||
- **Changing NULL columns to NON-NULL**
|
||||
|
||||
```sql
|
||||
UPDATE Channels SET LastRootPostAt=0 WHERE LastRootPostAt IS NULL;
|
||||
UPDATE OAuthApps SET MattermostAppID = '' WHERE MattermostAppID IS NULL;
|
||||
```
|
||||
|
||||
This is possible to handle from the code itself using a `COALESCE` function. It makes the code more complicated, but it’s a cost we have to pay to reduce migration overhead.
|
||||
|
||||
- **Denormalization optimizations**:
|
||||
|
||||
```sql
|
||||
UPDATE threads SET threaddeleteat = posts.deleteat FROM posts WHERE threads.threaddeleteat IS NULL AND posts.id = threads.postid;
|
||||
UPDATE reactions SET channelid = COALESCE((select channelid from posts where posts.id = reactions.postid), '') WHERE channelid='';
|
||||
UPDATE threads SET threadteamid = channels.teamid FROM channels WHERE threads.threadteamid IS NULL AND channels.id = threads.channelid;
|
||||
UPDATE fileinfo SET channelid = posts.channelid FROM posts WHERE fileinfo.channelid IS NULL AND fileinfo.postid = posts.id;
|
||||
```
|
||||
|
||||
We can take the same approach as in data migrations.
|
||||
|
||||
9. DELETE
|
||||
|
||||
So far, there have been only a handful of DELETE statements in schema migrations. And mostly they have been for security issues. The general recommendation is to avoid running a full-blown DELETE statement that operates on the entire table, but rather operate on batches so as to avoid taking a lock on the entire table. This could either be done in a job since there is no new code waiting for this to be executed. (See above)
|
||||
|
||||
## Examples
|
||||
|
||||
### How do I change a column type if I MUST
|
||||
|
||||
Follow this long-winded procedure:
|
||||
- Create a new column.
|
||||
- Migrate existing data.
|
||||
- From next ESR, start using the new column.
|
||||
- Next ESR, drop the old column.
|
||||
|
||||
For example, let’s say the next upcoming version is 8.4, and the next ESR is 8.6. So step 1 and 2, goes in 8.4. And in 8.7 onwards, we add the code to start using the new column, which will eventually be part of 8.12 (ESR after that). And then from 8.13 onwards, we can drop the column.
|
||||
|
||||
The following diagram should explain things better:
|
||||
|
||||

|
||||
|
||||
The reasoning behind this is some customers will only upgrade from ESR to ESR. So we need to ensure backwards compatibility with the previous version.
|
||||
|
||||
Following shows an example where we are adding a channel_count column to the status table. This is not exactly altering a column, but the idea remains the same, and you can extend this to fit your use-case.
|
||||
|
||||
1. ALTER TABLE status ADD COLUMN channel_count integer;
|
||||
|
||||
2. Our next objective is to migrate existing data. We do this in a 2-phase approach where we set up triggers to migrate all new data and in the background migrate old data in batches.
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION public.update_status_channel_count()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
member_count integer;
|
||||
BEGIN
|
||||
select count(*) into member_count from channelmembers where userid=NEW.userid;
|
||||
NEW.channel_count := member_count;
|
||||
RETURN NEW;
|
||||
END
|
||||
$function$
|
||||
|
||||
CREATE TRIGGER tr_update_status_channel_count
|
||||
BEFORE INSERT OR UPDATE ON status
|
||||
FOR EACH ROW EXECUTE PROCEDURE update_status_channel_count();
|
||||
```
|
||||
|
||||
After this is taken care of, we need to create a job, which will migrate existing data in batches.
|
||||
|
||||
```sql
|
||||
UPDATE status s SET channel_count=(SELECT count(*) FROM channelmembers cm WHERE cm.userid=s.userid) WHERE channel_count IS NOT NULL AND s.userid in (SELECT userid FROM status WHERE userid > '' ORDER BY userid ASC limit 10);
|
||||
```
|
||||
|
||||
Then store the user id offset in the job metadata.
|
||||
|
||||
```sql
|
||||
UPDATE status s SET channel_count=(SELECT count(*) FROM channelmembers cm WHERE cm.userid=s.userid) WHERE channel_count IS NOT NULL AND s.userid in (SELECT userid FROM status WHERE userid > <offset> ORDER BY userid ASC limit 10);
|
||||
```
|
||||
|
||||
At this point, when the job finishes, the new column would be ready to use. And the triggers would take care of always keeping the data up to date.
|
||||
|
||||
3. Now we can start using the new column from the next ESR version. But we cannot yet drop the existing column because of backwards compatibility guarantees. The old column would still be in use by older app nodes in the cluster during upgrade. We also would want to drop the trigger since its use is finished.
|
||||
|
||||
```sql
|
||||
DROP TRIGGER tr_update_status_channel_count on status
|
||||
```
|
||||
|
||||
4. And in the ESR after that, now we can finally drop the old column.
|
||||
|
||||
This deliberately skips renaming the column for simplicity. Depending on your use-case, you can do that if you want to. It is a fast operation that does not rebuild the table, so there are no issues.
|
||||
|
||||
### How do I add a unique constraint to a table
|
||||
|
||||
```sql
|
||||
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS oauthaccessdata_clientid_userid_key on oauthaccessdata(clientid, userid);
|
||||
ALTER TABLE oauthaccessdata ADD UNIQUE USING INDEX oauthaccessdata_clientid_userid_key; -- This is instantaneous
|
||||
```
|
||||
|
||||
### How do I run UPDATE statements in data migrations/denormalizations
|
||||
|
||||
The idea would be to run the UPDATE statements in batches so as to avoid taking a large lock. This is similar to the second part in the column change example.
|
||||
|
||||
Following is an example where I update the channels table in batches setting a new column.
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION public.update_in_batches()
|
||||
RETURNS INTEGER
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
id_offset text := '';
|
||||
rows_updated integer;
|
||||
BEGIN
|
||||
LOOP
|
||||
WITH table_holder AS (
|
||||
SELECT id FROM channels
|
||||
WHERE id > id_offset
|
||||
ORDER BY id ASC limit 100
|
||||
)
|
||||
UPDATE channels c SET new='improved' WHERE c.id in (SELECT id FROM table_holder); -- change this query to whatever your requirement is
|
||||
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
||||
|
||||
-- We have to run the select query again
|
||||
-- becaue "select into" isn't allowed inside a CTE
|
||||
-- and without CTE, we have to use a temp table (because you can't select into a table)
|
||||
-- and with a temp table, you run into max_locks_inside_transaction limit.
|
||||
-- Probably there is a better way but keeping things simple for now.
|
||||
select id into id_offset from (select id from channels where id > id_offset ORDER BY id ASC limit 100) as temp order by id desc limit 1;
|
||||
EXIT WHEN rows_updated = 0;
|
||||
END LOOP;
|
||||
return 1;
|
||||
END
|
||||
$function$;
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### I need to make a schema change. What do I do?
|
||||
|
||||
1. Add the appropriate SQL script file containing the statements you want to run into the migrations directory. This directory is located in `{project_dir}/db/migrations/{driver_name}/`.
|
||||
2. Run `make migrations-extract` to add your new migrations to the `db/migrations/migrations.list` file. This will ensure that there will be merge conflicts in case there is a conflict on migration sequence numbers with the master branch. Since we don't want to have a collision on version numbers of the migration files, the developer should merge the upstream branch to the feature branch just before merging so that we can be sure that there are no versioning issues. In case of a version number collision, the build process will fail and main branch will be broken until it gets fixed.
|
||||
3. When you run the mattermost/server binary, the tool will automatically apply the migration if it's required. The migration name will be saved in the `db_migrations` table.
|
||||
4. Lastly, please also measure the time taken for the migration with an eye towards resource usage. Please use the DB dumps from the ~developers-performance channel in our Community server. You will find the links in the channel header.
|
||||
5. In your PR, make sure to add release notes following the [Developer Schema Migration Template](https://docs.google.com/document/d/18lD7N32oyMtYjFrJKwsNv8yn6Fe5QtF-eMm8nn0O8tk/edit?tab=t.0).
|
||||
|
||||
### My migration has failed. What do I do?
|
||||
1. If you think your migration is applied, and you want to revert changes, you can run the down script to roll back in a clean way. You can use morph CLI to apply down migrations.
|
||||
- Before rolling down the script, check the `db_migrations` table whether the migration is applied or not.
|
||||
- If it's applied you can revert it using morph CLI command. An example command would look like `morph apply down --driver {your-driver} --dsn "{your-dsn}" --path {path-to-your-driver-specific-migration-files} --number 1`
|
||||
2. If the migration has been shipped in a release and you want to apply fixes, instead of changing the existing script, you should add a new one so that `db_migrations` will stay consistent. You can edit the existing migration to be a no-op for future releases in this case.
|
||||
|
||||
## GLOSSARY
|
||||
|
||||
- **DDL** - short form of Data Definition Language, which deals with database schema changes. For example: `CREATE TABLE`, `ALTER TABLE` etc.
|
||||
- **DML** - short form of Data Manipulation Language, which deals with SQL queries that read/update/delete tables. For example: `SELECT`, `UPDATE`, `INSERT` etc.
|
||||
- **FK** - Foreign Key
|
||||
|
||||
344
docs/develop/contribute/more-info/server/style-guide.md
Normal file
344
docs/develop/contribute/more-info/server/style-guide.md
Normal file
@@ -0,0 +1,344 @@
|
||||
---
|
||||
title: "Golang style guide"
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
Golang ("go") is a more opinionated language than many others when it comes to coding style. The compiler enforces some basic stylistic elements, such as the removal of unused variables and imports. Many others are enforced by the `gofmt` tool, such as usage of white-space, semicolons, indentation, and alignment. The `gofmt` tool is run over all code in the Mattermost Server CI pipeline. Any code which is not consistent with the formatting enforced by `gofmt` will not be accepted into the repository.
|
||||
|
||||
Despite this, there are still many areas of coding style which are not dictated by these tools. Rather than reinventing the wheel, we are adopting [Effective Go](https://golang.org/doc/effective_go.html) as a basis for our style guide. On top of that, we also follow the guidelines laid out by the Go project at [CodeReviewComments](https://go.dev/wiki/CodeReviewComments).
|
||||
|
||||
However, at present, some of the guidelines from these sources come into conflict with existing patterns that are present in our codebase which cannot immediately be corrected due to the need to maintain backward compatibility.
|
||||
|
||||
This document, which should be read in conjunction with [Effective Go](https://golang.org/doc/effective_go.html) and [CodeReviewComments](https://go.dev/wiki/CodeReviewComments), outlines the small number of exceptions we make to maintain backward compatibility, as well as a number of additional stylistic rules we have adopted on top of those external recommendations.
|
||||
|
||||
### Application of guidelines
|
||||
|
||||
The following guidelines should be applied to both new and existing code. However, this does not mean that a developer is *required* to fix any surrounding code that contravenes the rules in the style guide. It's encouraged to keep fixing things as you go, but it's not compulsory to do so. Reviewers should refrain from asking for stylistic changes in surrounding code if the submitter has not included them in their pull request.
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Project layout
|
||||
|
||||
When creating a new Go module, please follow the [standardized guidelines](https://go.dev/doc/modules/layout) of the Go team.
|
||||
|
||||
[This blog article](https://go.dev/blog/package-names) provides additional guidance on package names.
|
||||
|
||||
Following are some of the anti-patterns to keep in mind:
|
||||
|
||||
- Don't use the `pkg` pattern. This is a common standard used by many projects. But it goes against the Go philosophy of naming packages that signify what they contain. From https://blog.golang.org/package-names:
|
||||
|
||||
> A package's name provides context for its contents, making it easier for clients to understand what the package is for and how to use it.
|
||||
|
||||
The `pkg` directory was used long ago in the Go project when there weren't any well-defined standards. But it was later removed to just have normal packages.
|
||||
|
||||
- Same for `util` or `misc` packages. Don't use them. Instead break out related `util` functionalities into its own package or if it's not used by a lot of packages, make it part of the original package itself.
|
||||
|
||||
- Don't have too many small packages with only one file. It's usually a sign of splitting packages without giving thought into them. Look at the API boundaries and group the packages into bigger chunks.
|
||||
|
||||
### Functional
|
||||
|
||||
#### Default to sync instead of async
|
||||
|
||||
Always prefer synchronous functions by default. Async calls are hard to get right. They have no control over goroutine lifetimes and introduce data races. If you think something needs to be asynchronous, measure it and prove it. Ask these questions:
|
||||
|
||||
- Does it improve performance? If so, by how much?
|
||||
- What’s the tradeoff of the happy path vs. slow path?
|
||||
- How do I propagate errors?
|
||||
- What about back-pressure?
|
||||
- What should be my concurrency model?
|
||||
|
||||
Do not create one-off goroutines without knowing when/how they exit. They cause problems that are hard to debug, and can often cause performance degradation rather than an improvement. Have a look at:
|
||||
|
||||
- https://go.dev/wiki/CodeReviewComments#goroutine-lifetimes
|
||||
- https://go.dev/wiki/CodeReviewComments#synchronous-functions
|
||||
|
||||
#### Pointers to slices
|
||||
|
||||
Do not use pointers to slices. Slices are already reference types which point to an underlying array. If you want a function to modify a slice, then return that slice from the function, rather than passing a pointer.
|
||||
|
||||
#### Avoid creating more ToJSON methods
|
||||
|
||||
Do not create new `ToJSON` methods for model structs. Instead, just use `json.Marshal` at the call site. This has two major benefits:
|
||||
|
||||
- It avoids bugs due to the suppression of the JSON error which happens with `ToJSON` methods (we've had a number of bugs caused by this).
|
||||
- It's a common pattern to pass the output to something (like a network call) which accepts a byte-slice, leading to a double conversion from byte-slice to string to a byte-slice again if `ToJSON` methods are used.
|
||||
|
||||
#### [Interfaces](https://go.dev/wiki/CodeReviewComments#interfaces)
|
||||
|
||||
- Return structs, accept interfaces.
|
||||
- Interface names should end with “-er”. This is not a strict rule. Just a guideline which indicates the fact that interface functionalities are designed around the concept of “doing” something.
|
||||
- Try not to define interfaces on the implementer side of an API "for mocking"; instead, design the API so that it can be tested using the public API of the real implementation.
|
||||
|
||||
As an example, if you're trying to integrate with a third-party service, it's tempting to create an interface and use that in the code so that it can be easily mocked in the test. This is an anti-pattern and masks real bugs. Instead, you should try to use the real implementation via a docker container or if that's not feasible, mock the network response coming from the external process.
|
||||
|
||||
Another common pattern is to preemptively declare the interface in the source package itself, so that the consumer can just directly import the interface. Instead, try to declare the interface in the package which is going to consume the functionality. Often, different packages have non-overlapping set of functionalities to consume. If you do find several consumers of the package, remember that interfaces can be composed. So define small chunks of functionalities in different interfaces, and let consumers compose them as needed. Take a look at the set of interfaces in the [io](https://golang.org/pkg/io/) package.
|
||||
|
||||
These are just guidelines and not strict rules. Understand your use case and apply them appropriately.
|
||||
|
||||
### Stylistic
|
||||
|
||||
#### [CamelCase variables/constants](https://go.dev/wiki/CodeReviewComments#mixed-caps)
|
||||
|
||||
We use CamelCase names like WebsocketEventPostEdited, not WEBSOCKET_EVENT_POST_EDITED.
|
||||
|
||||
#### Empty string check
|
||||
|
||||
Use `foo == ""` to check for empty strings, not `len(foo) == 0`.
|
||||
|
||||
#### [Reduce indentation](https://go.dev/wiki/CodeReviewComments#indent-error-flow)
|
||||
|
||||
If there are multiple return statements in an if-else statement, remove the else block and outdent it.
|
||||
|
||||
This is an example from `mlog/human/parser.go`:
|
||||
|
||||
```go
|
||||
// Look for an initial "{"
|
||||
if token, err := dec.Token(); err != nil {
|
||||
return result, err
|
||||
} else {
|
||||
d, ok := token.(json.Delim)
|
||||
if !ok || d != '{' {
|
||||
return result, fmt.Errorf("input is not a JSON object, found: %v", token)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This can be simplified to:
|
||||
|
||||
```go
|
||||
// Look for an initial "{"
|
||||
if token, err := dec.Token(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
d, ok := token.(json.Delim)
|
||||
if !ok || d != '{' {
|
||||
return result, fmt.Errorf("input is not a JSON object, found: %v", token)
|
||||
}
|
||||
```
|
||||
|
||||
#### [Initialisms](https://go.dev/wiki/CodeReviewComments#initialisms)
|
||||
|
||||
Use `userID` rather than `userId`. Same for abbreviations; `HTTP` is preferred over `Http` or `http`.
|
||||
|
||||
#### [Receiver names](https://go.dev/wiki/CodeReviewComments#receiver-names)
|
||||
|
||||
The name of a method's receiver should be a reflection of its identity; often a one or two letter abbreviation of its type suffices (such as "c" or "cl" for "Client"). Don't use generic names such as "me", "this", or "self" identifiers typical of object-oriented languages that give the variable a special meaning.
|
||||
|
||||
#### Error variable names
|
||||
|
||||
The name of any `error` variable must be `err` or prefixed with `err`. The name of any `*model.AppError` variable must be `appErr` or prefixed with `appErr`. This allows us to avoid confusion about how to handle different kind of errors inside Mattermost. If you are storing the error value from a function that returns an `error` type in its signature, it's considered an `error`, regardless of whether the function, internally, is returning a `*model.AppError` instance.
|
||||
|
||||
For example, when the function signature returns an `error`, we use the `err` variable name:
|
||||
|
||||
```go
|
||||
func MyFunction() error {
|
||||
return model.NewAppError(...)
|
||||
}
|
||||
|
||||
func OtherFunction() {
|
||||
...
|
||||
err := MyFunction()
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
When the function signature returns an `*model.AppError`, we use the `appErr` variable name:
|
||||
|
||||
```go
|
||||
func MyFunction() *model.AppError {
|
||||
return model.NewAppError(...)
|
||||
}
|
||||
|
||||
func OtherFunction() {
|
||||
...
|
||||
appErr := MyFunction()
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Errors
|
||||
|
||||
Always add proper context to errors.
|
||||
|
||||
#### Include the user input for validation errors.
|
||||
|
||||
For example:
|
||||
|
||||
`return fmt.Errorf("invalid export type, must be one of: csv, actiance, globalrelay")` does not include what was the input value entered by the user. A better way might be:
|
||||
|
||||
```diff
|
||||
- return fmt.Errorf("invalid export type, must be one of: csv, actiance, globalrelay")
|
||||
+ return fmt.Errorf("invalid export type: %s, must be one of: csv, actiance, globalrelay", exportType)
|
||||
```
|
||||
|
||||
Another example:
|
||||
|
||||
```diff
|
||||
if r.Start > r.End {
|
||||
- return fmt.Errorf("report timestamps are erroneous")
|
||||
+ return fmt.Errorf("report timestamps are erroneous: start_timestamp %f is greater than end_timestamp %f", r.Start, r.End)
|
||||
}
|
||||
```
|
||||
#### Return errors instead of boolean for ID validation errors
|
||||
|
||||
A validation function can check various properties of an object. But if we simply return true or false, and then log that object is invalid, the user will have no idea why it is invalid or how to fix it.
|
||||
|
||||
For example:
|
||||
|
||||
```diff
|
||||
- func IsValidId(value string) bool {
|
||||
+ func IsValidId(value string) error {
|
||||
if len(value) != 26 {
|
||||
- return false
|
||||
+ return fmt.Errorf("Invalid length. Found: %d; expected: %d", len(value), 26)
|
||||
}
|
||||
|
||||
for _, r := range value {
|
||||
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
|
||||
- return false
|
||||
+ return fmt.Errorf("Rune %c in %s is not an unicode letter or number", r, value)
|
||||
}
|
||||
}
|
||||
|
||||
- return true
|
||||
+ return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
Log messages should be annotated with contextual information in the form of key-value pairs to make it easier to identify the context they originated from. The keys should use snake_case. Refer to the corresponding JSON struct tags for key names.
|
||||
|
||||
```go
|
||||
func (a *App) SendNotifications(...) {
|
||||
..
|
||||
_, err := a.sendOutOfChannelMentions(c, sender, post, channel, ...)
|
||||
if err != nil {
|
||||
c.Logger().Error(
|
||||
"Failed to send warning for out of channel mentions",
|
||||
mlog.String("user_id", sender.Id),
|
||||
mlog.String("post_id", post.Id),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
..
|
||||
}
|
||||
```
|
||||
|
||||
#### Avoid double-logging
|
||||
|
||||
Double-logging is when you immediately log something after an error, and also return an error at the same time. This creates two log lines with the same error and is confusing to the admin. The best practice is to always pass the error upwards adding context and log it in the upper-most layer correct. Logging the error at the lowest layer does not give any additional context as to from where it was called, and what path did the code take to reach there.
|
||||
|
||||
For example:
|
||||
|
||||
```diff
|
||||
if err != nil {
|
||||
- mlog.Error("Failed to generate SQL query",
|
||||
- mlog.String("user_id", userID),
|
||||
- mlog.Int("timestamp", int(syncTime)),
|
||||
- mlog.Err(err),
|
||||
- )
|
||||
- return errors.Wrap(err, "failed to generate SQL query")
|
||||
+ return errors.Wrapf(err, "failed to generate SQL query: user_id: %s, timestamp: %d", userID, int(syncTime))
|
||||
}
|
||||
```
|
||||
|
||||
#### Log levels
|
||||
|
||||
The purpose of logging is to provide observability - it enables the application communicate back to the administrator about what is happening. To communicate effectively logs should be meaningful and concise. To achieve this, log lines should conform to one of the definitions below:
|
||||
|
||||
**Critical:** This log-level represents the most severe situations when the service is entirely unable to continue operating. After emitting a _critical_ log line, it is expected that the service will terminate.
|
||||
|
||||
For example, the code block below demonstrates a _critical_ situation where the server startup routine fails, meaning the service is unable to start and must terminate.
|
||||
|
||||
```go
|
||||
func runServer(..) {
|
||||
..
|
||||
server, err := app.NewServer(options...)
|
||||
if err != nil {
|
||||
mlog.Critical(err.Error())
|
||||
return
|
||||
}
|
||||
..
|
||||
}
|
||||
```
|
||||
|
||||
**Error:** This log-level is used when something unexpected has happened to the service, but it does not result in a total loss of service. Log lines using the _error_ level must be actionable, so that the system administrator can investigate and resolve the incident. The _error_ log level may indicate a loss of service for an individual user or request or it may indicate a total failure of a non-critical subsystem within the service.
|
||||
|
||||
For example, the _error_ log level is used in the code snippet below as it represents a partial failure of one non-critical subsystem of the service. Administrator intervention is required to resolve this situation, but the rest of the service is able to continue operating in the meantime.
|
||||
|
||||
```go
|
||||
func (a *App) SyncPlugins(..) {
|
||||
..
|
||||
reader, appErr := a.FileReader(plugin.path)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr))
|
||||
return
|
||||
}
|
||||
..
|
||||
}
|
||||
```
|
||||
|
||||
**Warn:** This log level is used to indicate that something unexpected has happened, but the server is able to continue operating and it has not suffered any loss of functionality as a consequence of this failure. System administrators may wish to investigate the cause of log lines at this level, but the need is typically less pressing than for those at _error_ level. System administrators may also wish to monitor the rate of occurrence of individual log-lines at this level as this may be indicative of a wider problem. Log lines at the _warning_ level should be as detailed as possible, since these are often the least clear-cut category of message.
|
||||
|
||||
For example, the _warning_ log level may be used to indicate that something went wrong but the overall operation was still able to complete successfully.
|
||||
|
||||
```go
|
||||
func (a *App) UpdateUserRoles(..) {
|
||||
..
|
||||
if result := <-schan; result.NErr != nil {
|
||||
// soft error since the user roles were still updated
|
||||
mlog.Warn("Error during updating user roles", mlog.Err(result.NErr))
|
||||
}
|
||||
|
||||
a.InvalidateCacheForUser(userId)
|
||||
..
|
||||
}
|
||||
```
|
||||
|
||||
**Info:** This log level should be used to record normal, expected application behavior, even if it results in an error for the end user. They are not actionable individually, but the significant changes in the frequency of occurrence of individual log lines at this level may be indicative of a possible problem.
|
||||
|
||||
For example, the _info_ log level may be used to communicate to administrators that certain subsystems within the service have been started or stopped.
|
||||
|
||||
```go
|
||||
func (s *Schedulers) Start(..) {
|
||||
s.startOnce.Do(func() {
|
||||
mlog.Info("Starting schedulers.")
|
||||
..
|
||||
})
|
||||
..
|
||||
}
|
||||
```
|
||||
|
||||
**Debug:** This log-level is used for diagnostic information which may be used to debug issues but is not necessary for normal production system logging, nor actionable by system administrators.
|
||||
|
||||
```go
|
||||
func (worker *Worker) Run() {
|
||||
mlog.Debug("Worker started", mlog.String("worker", worker.name))
|
||||
..
|
||||
```
|
||||
|
||||
### Performance sensitive areas
|
||||
|
||||
Any PR that can potentially have a performance impact on the `mattermost/server` codebase is encouraged to have a performance review. For more information, please see this [link](https://docs.google.com/document/d/1Uzt3XHyKhDKipkuCmESkHoPio7vz7VYS4N_5_9ffgNU/edit). The following is a brief list of indicators that should to undergo a performance review:
|
||||
|
||||
- New features that might require benchmarks and/or are missing load-test coverage.
|
||||
- PRs touching performance of critical parts of the codebase (e.g. `Hub`/`WebConn`).
|
||||
- PRs adding or updating SQL queries.
|
||||
- Creating goroutines.
|
||||
- Doing potentially expensive allocations: `bytes.Buffer` and `[]byte`:
|
||||
- Use of `Buffer.Grow`, `Buffer.ReadFrom`, `ioutil.ReadAll`.
|
||||
- Creating big slices and maps without capacity when size is known in advance.
|
||||
- Recursion, unbounded, and deeply nested `for` loops.
|
||||
- Use of locks and/or other synchronization primitives.
|
||||
- Regular expressions, especially when creating `regexp.MustCompile` dynamically every time.
|
||||
- Use of the `reflect` package.
|
||||
|
||||
## Propose a new rule
|
||||
|
||||
To propose a new rule, follow the process below:
|
||||
|
||||
- Add it to the agenda in the [Server](https://community.mattermost.com/core/channels/developers-server) Guild meeting, and propose it.
|
||||
- If it gets accepted, create a go-vet rule (if possible), or a golangci-lint rule to prevent new regressions from creeping in.
|
||||
- Fix all existing issues.
|
||||
- Add it to this guide.
|
||||
21
docs/develop/contribute/more-info/server/system_console.md
Normal file
21
docs/develop/contribute/more-info/server/system_console.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: "System Console"
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
## Add fields to the configuration
|
||||
|
||||
In order to add fields to the configuration, you need to modify `model/config.go` in the server by adding the desired field to one of the structs such as `ServiceSettings` and setting its default value in the corresponding `SetDefaults` method.
|
||||
|
||||
Note that some of the configuration values are collected as telemetries. The telemetry definitions are defined in the `services/telemetry` package. Once a configuration is added, it should be added to the telemetry package. If the configuration value is not going to be collected as a telemetry, a `// telemetry: none` comment must be added to prevent the [configtelemetry](https://github.com/mattermost/mattermost-govet#included-analyzers) check from failing.
|
||||
|
||||
Also we use struct tags to identify access level for configuration values. If the value requires a restriction, please use this tag accordingly.
|
||||
|
||||
### Expose settings in the System Console
|
||||
|
||||
To expose the newly-added field in the System Console, you need to add that same setting to the `AdminDefinition` JS object in `webapp/channels/src/components/admin_console/admin_definition.jsx`. This object defines most of the settings in the System Console.
|
||||
|
||||
|
||||
### Make settings available for non-admin users
|
||||
|
||||
To make the newly added setting accessible to non-admin users in the apps, you'll need to add it to the `GenerateClientConfig` method in `config/client.go` in the server. Note that this always encodes the setting as a string, so anywhere that you would want to use this value in the client, you have to look for a string.
|
||||
122
docs/develop/contribute/more-info/server/tests.md
Normal file
122
docs/develop/contribute/more-info/server/tests.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: "Tests"
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
## Handling Flaky Tests
|
||||
|
||||
A flaky test is one that exhibits both passing and failing results when run multiple times without any code changes. When our automation detects a flaky test on your PR:
|
||||
|
||||
1. **Check if the Test is Newly Introduced**
|
||||
- Review your PR changes to determine if the flaky test was introduced by your changes
|
||||
- If the test is new, fix the flakiness in your PR before merging
|
||||
|
||||
2. **For Existing Flaky Tests**
|
||||
- Create a JIRA ticket titled "Flaky Test: \{TestName\}", e.g. "Flaky Test: TestGetMattermostLog"
|
||||
- Copy the test failure message into the JIRA ticket description
|
||||
- Add the `flaky-test` and `triage-global` labels
|
||||
- Create a PR to skip the test by adding:
|
||||
|
||||
```go
|
||||
t.Skip("https://mattermost.atlassian.net/browse/MM-XXXXX")
|
||||
```
|
||||
|
||||
where MM-XXXXX is your JIRA ticket number
|
||||
- Link the JIRA ticket in the skip message for tracking
|
||||
|
||||
This process helps us track and systematically address flaky tests while preventing them from blocking development work.
|
||||
|
||||
## Writing Parallel Tests
|
||||
|
||||
Leveraging parallel tests can drastically reduce execution time for entire test packages, such as [`api4`](https://github.com/mattermost/mattermost/tree/master/server/channels/api4) and [`app`](https://github.com/mattermost/mattermost/tree/master/server/channels/app), which are notably heavy with hundreds of tests. However, careful implementation is essential to ensure reliability and prevent flakiness. Follow these guidelines when writing parallel tests:
|
||||
|
||||
### Enabling Parallel Tests
|
||||
|
||||
In [`api4`](https://github.com/mattermost/mattermost/tree/master/server/channels/api4), [`app`](https://github.com/mattermost/mattermost/tree/master/server/channels/app), [`platform`](https://github.com/mattermost/mattermost/tree/master/server/channels/app/platform), [`email`](https://github.com/mattermost/mattermost/tree/master/server/channels/app/email), [`jobs`](https://github.com/mattermost/mattermost/tree/master/server/channels/jobs) packages:
|
||||
|
||||
```go
|
||||
func TestExample(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
// OR
|
||||
|
||||
func TestExample(t *testing.T) {
|
||||
th := Setup(t)
|
||||
th.Parallel(t)
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
// OR
|
||||
|
||||
func TestExample(t *testing.T) {
|
||||
if mainHelper.Options.RunParallel {
|
||||
t.Parallel()
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If [`sqlstore`](https://github.com/mattermost/mattermost/tree/master/server/channels/store/sqlstore) package:
|
||||
|
||||
```go
|
||||
func TestExample(t *testing.T) {
|
||||
if enableFullyParallelTests {
|
||||
t.Parallel()
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
To enable parallel execution, you should set the `ENABLE_FULLY_PARALLEL_TESTS` environment variable. Example:
|
||||
|
||||
```bash
|
||||
ENABLE_FULLY_PARALLEL_TESTS=true go test -v ./api4/...
|
||||
```
|
||||
|
||||
### When to Use Parallel Tests
|
||||
|
||||
- **Generally Safe**: Tests with dedicated setup functions that ensure independence from other tests.
|
||||
- **Subtests**: Only safe if each subtest features its own setup function, ensuring they are decoupled and independent of execution order.
|
||||
- **Unsafe**: When a subtest depends on state changes made by another subtest, thus coupling their execution order.
|
||||
|
||||
### Common Issues That Break Parallel Safety
|
||||
|
||||
#### Global State
|
||||
|
||||
Avoid reliance on global variables and registrations such as:
|
||||
|
||||
- `LicenseValidator`
|
||||
- `platform.RegisterMetricsInterface`
|
||||
- `platform.PurgeLinkCache`
|
||||
- `model.BuildEnterpriseReady`
|
||||
- `jobs.DefaultWatcherPollingInterval`
|
||||
|
||||
#### Filesystem Operations
|
||||
|
||||
Avoid using `os.Chdir` (or `t.Chdir`) and relative paths tied to the test executable, as they may introduce inconsistencies when tests run in parallel. When possible, rely on temporary directories such as `th.tempWorkspace` which are dedicated to the test.
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
Using `os.Setenv` for feature flags and other settings can cause interference between parallel tests. Instead, use the configuration API:
|
||||
|
||||
```go
|
||||
// UNSAFE for parallel tests:
|
||||
os.Setenv("MM_FEATUREFLAGS_CUSTOMFEATURE", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMFEATURE")
|
||||
|
||||
// SAFE for parallel tests:
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.FeatureFlags.CustomFeature = true
|
||||
})
|
||||
```
|
||||
|
||||
#### Process-Level Methods
|
||||
|
||||
Be cautious with methods affecting the entire process, such as `pprof.StartCPUProfile`, which can introduce contention between tests.
|
||||
23
docs/develop/contribute/more-info/server/tooling.md
Normal file
23
docs/develop/contribute/more-info/server/tooling.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: "Tools"
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
## Mattermost Server
|
||||
|
||||
In the [mattermost repository](https://github.com/mattermost/mattermost), we are using [Docker](https://www.docker.com/) images and [Docker Compose](https://docs.docker.com/compose/) to set up the development enviroment. The following are required images:
|
||||
|
||||
- [MySQL](https://www.mysql.com/)
|
||||
- [PostgreSQL](https://www.postgresql.org/)
|
||||
- [MinIO](https://min.io/)
|
||||
- [Inbucket](https://www.inbucket.org/)
|
||||
- [OpenLDAP](https://www.openldap.org/)
|
||||
- [Elasticsearch](https://www.elastic.co)
|
||||
|
||||
We also have added optional tools to help with your development:
|
||||
|
||||
### Dejavu
|
||||
|
||||
[Dejavu](https://opensource.appbase.io/dejavu/) is a user interface for Elasticsearch when no UI is provided to visualize or modify the data you're storing inside Elasticsearch.
|
||||
|
||||
To use Dejavu, execute `docker-compose up -d dejavu`. It will run at `http://localhost:1358`.
|
||||
Reference in New Issue
Block a user