build(deps): bump github.com/nats-io/nats-server/v2

Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.14.0 to 2.14.2.
- [Release notes](https://github.com/nats-io/nats-server/releases)
- [Changelog](https://github.com/nats-io/nats-server/blob/main/RELEASES.md)
- [Commits](https://github.com/nats-io/nats-server/compare/v2.14.0...v2.14.2)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats-server/v2
  dependency-version: 2.14.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot] authored and GitHub committed 2026-06-04 10:15:07 +00:00
1 parent 4414bd0d08
commit 5f88cc686c
58 files changed
+3201 -6425

No files matched your search

+5 -5
View File
@@ -55,7 +55,7 @@ require (
github.com/libregraph/lico v0.66.0
github.com/mna/pigeon v1.3.0
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/nats-io/nats-server/v2 v2.14.0
github.com/nats-io/nats-server/v2 v2.14.2
github.com/nats-io/nats.go v1.51.0
github.com/olekukonko/tablewriter v1.1.4
github.com/onsi/ginkgo v1.16.5
@@ -102,7 +102,7 @@ require (
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
golang.org/x/crypto v0.51.0
golang.org/x/crypto v0.52.0
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/image v0.40.0
golang.org/x/net v0.55.0
@@ -256,7 +256,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/juliangruber/go-intersect v1.1.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
@@ -303,8 +303,8 @@ require (
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/jwt/v2 v2.8.1 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/jwt/v2 v2.8.2 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/oklog/run v1.2.0 // indirect
+10 -10
View File
@@ -719,8 +719,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -898,14 +898,14 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
github.com/nats-io/nats-server/v2 v2.14.0 h1:+8q0HrDFotwLLcGH/legOEOnowunhK+aZ4GYBIWpQlM=
github.com/nats-io/nats-server/v2 v2.14.0/go.mod h1:ImVUUDvfClJbb6cuJQRc1VmgDCXKM5ds0OoiG9MVOKo=
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
github.com/nats-io/nats-server/v2 v2.14.2 h1:Q7dRhCY03Y00rETFW3KV+KGaCIajlDfWgWUVgbMxyuk=
github.com/nats-io/nats-server/v2 v2.14.2/go.mod h1:lWpb1bSpRELZfRdlMkdz8E7lbXKKyNe8RIn0vvepIHs=
github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI=
github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
@@ -1362,8 +1362,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+103
View File
@@ -0,0 +1,103 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1765288076,
"owner": "cachix",
"repo": "devenv",
"rev": "93c055af1e8fcac49251f1b2e1c57f78620ad351",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1765121682,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1765016596,
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "548fc44fca28a5e81c5d6b846e555e6b9c2a5a3c",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1762808025,
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1764580874,
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "dcf61356c3ab25f1362b4a4428a6d871e84f1d1d",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": [
"git-hooks"
]
}
}
},
"root": "root",
"version": 7
}
+14
View File
@@ -0,0 +1,14 @@
{
pkgs,
...
}:
{
languages = {
go.enable = true;
};
packages = with pkgs; [
golangci-lint
];
}
+4
View File
@@ -0,0 +1,4 @@
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
+1
View File
@@ -1,2 +1,3 @@
* -text
*.bin -text -diff
*.md text eol=lf
+700 -700
View File
File diff suppressed because it is too large. Load diff
+78 -78
View File
@@ -1,79 +1,79 @@
# Finite State Entropy
This package provides Finite State Entropy encoding and decoding.
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
encoding provides a fast near-optimal symbol encoding/decoding
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
## News
* Feb 2018: First implementation released. Consider this beta software for now.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `(error)` | An internal error occurred. |
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
# Performance
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
beneficial to transpose all your input values down by 64.
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
around 300MB/s decompression speed.
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
# Plans
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
# Finite State Entropy
This package provides Finite State Entropy encoding and decoding.
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
encoding provides a fast near-optimal symbol encoding/decoding
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
## News
* Feb 2018: First implementation released. Consider this beta software for now.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `(error)` | An internal error occurred. |
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
# Performance
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
beneficial to transpose all your input values down by 64.
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
around 300MB/s decompression speed.
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
# Plans
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
+89 -89
View File
@@ -1,89 +1,89 @@
# Huff0 entropy compression
This package provides Huff0 encoding and decoding as used in zstd.
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
## News
This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
This ensures that most functionality is well tested.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
| `(error)` | An internal error occurred. |
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
## Tables and re-use
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
that controls this behaviour. See the documentation for details. This can be altered between each block.
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
based on the boolean reported back from the CompressXX call.
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
## Decompressing
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
This will initialize the decoding tables.
You can supply the complete block to `ReadTable` and it will return the data part of the block
which can be given to the decompressor.
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
# Huff0 entropy compression
This package provides Huff0 encoding and decoding as used in zstd.
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
## News
This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
This ensures that most functionality is well tested.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
| `(error)` | An internal error occurred. |
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
## Tables and re-use
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
that controls this behaviour. See the documentation for details. This can be altered between each block.
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
based on the boolean reported back from the CompressXX call.
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
## Decompressing
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
This will initialize the decoding tables.
You can supply the complete block to `ReadTable` and it will return the data part of the block
which can be given to the decompressor.
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
+1 -1
View File
@@ -51,7 +51,7 @@
//
// The d variable is implicitly R_DST - R_DBASE, and len(dst)-d is R_DEND - R_DST.
// The s variable is implicitly R_SRC - R_SBASE, and len(src)-s is R_SEND - R_SRC.
TEXT ·s2Decode(SB), NOSPLIT, $48-56
TEXT ·s2Decode(SB), NOSPLIT, $56-56
// Initialize R_SRC, R_DST and R_DBASE-R_SEND.
MOVQ dst_base+0(FP), R_DBASE
MOVQ dst_len+8(FP), R_DLEN
+16 -12
View File
@@ -23,12 +23,12 @@ func encodeBlockBest(dst, src []byte, dict *Dict) (d int) {
// Initialize the hash tables.
const (
// Long hash matches.
lTableBits = 19
maxLTableSize = 1 << lTableBits
lTableBits = bestLongTableBits
maxLTableSize = bestLongTableSize
// Short hash matches.
sTableBits = 16
maxSTableSize = 1 << sTableBits
sTableBits = bestShortTableBits
maxSTableSize = bestShortTableSize
inputMargin = 8 + 2
@@ -44,8 +44,10 @@ func encodeBlockBest(dst, src []byte, dict *Dict) (d int) {
}
sLimitDict := min(len(src)-inputMargin, MaxDictSrcOffset-inputMargin)
var lTable [maxLTableSize]uint64
var sTable [maxSTableSize]uint64
tbl := getBestTables()
lTable := &tbl.lTable
sTable := &tbl.sTable
defer bestTablePool.Put(tbl)
// Bail if we can't compress to at least this.
dstLimit := len(src) - 5
@@ -456,12 +458,12 @@ func encodeBlockBestSnappy(dst, src []byte) (d int) {
// Initialize the hash tables.
const (
// Long hash matches.
lTableBits = 19
maxLTableSize = 1 << lTableBits
lTableBits = bestLongTableBits
maxLTableSize = bestLongTableSize
// Short hash matches.
sTableBits = 16
maxSTableSize = 1 << sTableBits
sTableBits = bestShortTableBits
maxSTableSize = bestShortTableSize
inputMargin = 8 + 2
)
@@ -474,8 +476,10 @@ func encodeBlockBestSnappy(dst, src []byte) (d int) {
return 0
}
var lTable [maxLTableSize]uint64
var sTable [maxSTableSize]uint64
tbl := getBestTables()
lTable := &tbl.lTable
sTable := &tbl.sTable
defer bestTablePool.Put(tbl)
// Bail if we can't compress to at least this.
dstLimit := len(src) - 5
+24 -18
View File
@@ -59,16 +59,18 @@ func encodeBlockBetterGo(dst, src []byte) (d int) {
// Initialize the hash tables.
const (
// Long hash matches.
lTableBits = 17
maxLTableSize = 1 << lTableBits
lTableBits = betterLongTableBits
maxLTableSize = betterLongTableSize
// Short hash matches.
sTableBits = 14
maxSTableSize = 1 << sTableBits
sTableBits = betterShortTableBits
maxSTableSize = betterShortTableSize
)
var lTable [maxLTableSize]uint32
var sTable [maxSTableSize]uint32
tbl := getBetterTables()
lTable := &tbl.lTable
sTable := &tbl.sTable
defer betterTablePool.Put(tbl)
// Bail if we can't compress to at least this.
dstLimit := len(src) - len(src)>>5 - 6
@@ -317,16 +319,18 @@ func encodeBlockBetterSnappyGo(dst, src []byte) (d int) {
// Initialize the hash tables.
const (
// Long hash matches.
lTableBits = 16
maxLTableSize = 1 << lTableBits
lTableBits = betterSnappyLongTableBits
maxLTableSize = betterSnappyLongTableSize
// Short hash matches.
sTableBits = 14
maxSTableSize = 1 << sTableBits
sTableBits = betterShortTableBits
maxSTableSize = betterShortTableSize
)
var lTable [maxLTableSize]uint32
var sTable [maxSTableSize]uint32
tbl := getBetterSnappyTables()
lTable := &tbl.lTable
sTable := &tbl.sTable
defer betterSnappyTablePool.Put(tbl)
// Bail if we can't compress to at least this.
dstLimit := len(src) - len(src)>>5 - 6
@@ -902,12 +906,12 @@ func encodeBlockBetterDict(dst, src []byte, dict *Dict) (d int) {
// Initialize the hash tables.
const (
// Long hash matches.
lTableBits = 17
maxLTableSize = 1 << lTableBits
lTableBits = betterLongTableBits
maxLTableSize = betterLongTableSize
// Short hash matches.
sTableBits = 14
maxSTableSize = 1 << sTableBits
sTableBits = betterShortTableBits
maxSTableSize = betterShortTableSize
maxAhead = 8 // maximum bytes ahead without checking sLimit
@@ -921,8 +925,10 @@ func encodeBlockBetterDict(dst, src []byte, dict *Dict) (d int) {
dict.initBetter()
var lTable [maxLTableSize]uint32
var sTable [maxSTableSize]uint32
tbl := getBetterTables()
lTable := &tbl.lTable
sTable := &tbl.sTable
defer betterTablePool.Put(tbl)
// Bail if we can't compress to at least this.
dstLimit := len(src) - len(src)>>5 - 6
+65
View File
@@ -0,0 +1,65 @@
package s2
import "sync"
// Table size constants
const (
betterLongTableBits = 17
betterLongTableSize = 1 << betterLongTableBits // 131072
betterShortTableBits = 14
betterShortTableSize = 1 << betterShortTableBits // 16384
betterSnappyLongTableBits = 16
betterSnappyLongTableSize = 1 << betterSnappyLongTableBits // 65536
bestLongTableBits = 19
bestLongTableSize = 1 << bestLongTableBits // 524288
bestShortTableBits = 16
bestShortTableSize = 1 << bestShortTableBits // 65536
)
type betterTables struct {
lTable [betterLongTableSize]uint32
sTable [betterShortTableSize]uint32
}
var betterTablePool = sync.Pool{New: func() interface{} { return &betterTables{} }}
// betterSnappyTables holds better-snappy compression hash tables.
type betterSnappyTables struct {
lTable [betterSnappyLongTableSize]uint32
sTable [betterShortTableSize]uint32
}
var betterSnappyTablePool = sync.Pool{New: func() interface{} { return &betterSnappyTables{} }}
// bestTables holds best compression hash tables.
type bestTables struct {
lTable [bestLongTableSize]uint64
sTable [bestShortTableSize]uint64
}
var bestTablePool = sync.Pool{New: func() interface{} { return &bestTables{} }}
// getBetterTables gets a zeroed betterTables from the pool.
func getBetterTables() *betterTables {
t := betterTablePool.Get().(*betterTables)
*t = betterTables{}
return t
}
// getBetterSnappyTables gets a zeroed betterSnappyTables from the pool.
func getBetterSnappyTables() *betterSnappyTables {
t := betterSnappyTablePool.Get().(*betterSnappyTables)
*t = betterSnappyTables{}
return t
}
// getBestTables gets a zeroed bestTables from the pool.
func getBestTables() *bestTables {
t := bestTablePool.Get().(*bestTables)
*t = bestTables{}
return t
}
+2
View File
@@ -347,9 +347,11 @@ func NewAccountClaims(subject string) *AccountClaims {
c.SigningKeys = make(SigningKeys)
// Set to unlimited to start. We do it this way so we get compiler
// errors if we add to the OperatorLimits.
// JetStream is disabled by default by setting MemoryStorage and DiskStorage to zero, instead of NoLimit.
c.Limits = OperatorLimits{
NatsLimits{NoLimit, NoLimit, NoLimit},
AccountLimits{NoLimit, NoLimit, true, false, NoLimit, NoLimit},
// Default zeros implies that JetStream is not enabled by default, see OperatorLimits.IsJSEnabled().
JetStreamLimits{0, 0, 0, 0, 0, 0, 0, false},
JetStreamTieredLimits{},
}
+2 -2
View File
@@ -66,7 +66,7 @@ type Account struct {
claimJWT string
updated time.Time
mu sync.RWMutex
sqmu sync.Mutex
smu sync.Mutex // serializes route interest updates
sl *Sublist
ic *client
sq *sendq
@@ -80,7 +80,7 @@ type Account struct {
nrleafs int32
clients map[*client]struct{}
rm map[string]int32
lqws map[string]int32
lws map[string]int32 // per key, last rm[key] sent to routes; used to dedup sends
usersRevoked map[string]int64
mappings []*mapping
hasMapped atomic.Bool
+3 -1
View File
@@ -56,6 +56,8 @@ type ClientAuthentication interface {
GetNonce() []byte
// Kind indicates what type of connection this is matching defined constants like CLIENT, ROUTER, GATEWAY, LEAF etc
Kind() int
//Gets the ID associated with a client
GetID() uint64
}
// NkeyUser is for multiple nkey based users
@@ -1283,7 +1285,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool {
hasEmailAddresses := len(cert.EmailAddresses) > 0
hasSubject := len(cert.Subject.String()) > 0
hasURIs := len(cert.URIs) > 0
if !hasEmailAddresses && !hasSubject && !hasURIs {
if !hasSANs && !hasEmailAddresses && !hasSubject && !hasURIs {
c.Debugf("User required in cert, none found")
return false
}
+120 -52
View File
@@ -30,6 +30,7 @@ import (
"net/url"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
"sync"
@@ -270,7 +271,7 @@ type client struct {
mpay int32
msubs int32
mcl int32
mu sync.Mutex
mu sync.RWMutex
cid uint64
start time.Time
nonce []byte
@@ -560,6 +561,13 @@ func (c *client) GetNonce() []byte {
return c.nonce
}
// GetID returns the client ID
func (c *client) GetID() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.cid
}
// GetName returns the application supplied name for the connection.
func (c *client) GetName() string {
c.mu.Lock()
@@ -998,6 +1006,7 @@ func (c *client) RegisterUser(user *User) {
// Reset perms to nil in case client previously had them.
c.perms = nil
c.mperms = nil
c.darray = nil
} else {
c.setPermissions(user.Permissions)
}
@@ -1035,6 +1044,7 @@ func (c *client) RegisterNkeyUser(user *NkeyUser) error {
// Reset perms to nil in case client previously had them.
c.perms = nil
c.mperms = nil
c.darray = nil
} else {
c.setPermissions(user.Permissions)
}
@@ -1061,6 +1071,8 @@ func (c *client) setPermissions(perms *Permissions) {
return
}
c.perms = &permissions{}
c.mperms = nil
c.darray = nil
slcache := c.srv != nil && !c.srv.getOpts().NoSublistCache
// Loop over publish permissions
@@ -1092,7 +1104,7 @@ func (c *client) setPermissions(perms *Permissions) {
if perms.Subscribe != nil {
var err error
if len(perms.Subscribe.Allow) > 0 {
c.perms.sub.allow = NewSublist(slcache)
c.perms.sub.allow = NewSublistNoCache()
}
for _, subSubject := range perms.Subscribe.Allow {
sub := &subscription{}
@@ -1104,7 +1116,7 @@ func (c *client) setPermissions(perms *Permissions) {
c.perms.sub.allow.Insert(sub)
}
if len(perms.Subscribe.Deny) > 0 {
c.perms.sub.deny = NewSublist(slcache)
c.perms.sub.deny = NewSublistNoCache()
// Also hold onto this array for later.
c.darray = perms.Subscribe.Deny
}
@@ -1201,38 +1213,40 @@ func (c *client) mergeDenyPermissions(what denyType, denyPubs []string) {
if c.perms == nil {
c.perms = &permissions{}
}
slcache := c.srv != nil && !c.srv.getOpts().NoSublistCache
var perms []*perm
switch what {
case pub:
perms = []*perm{&c.perms.pub}
case sub:
perms = []*perm{&c.perms.sub}
case both:
perms = []*perm{&c.perms.pub, &c.perms.sub}
}
for _, p := range perms {
if p.deny == nil {
p.deny = NewSublist(slcache)
if what == pub || what == both {
if c.perms.pub.deny == nil {
c.perms.pub.deny = NewSublistForServer(c.srv)
}
FOR_DENY:
for _, subj := range denyPubs {
r := p.deny.Match(subj)
for _, v := range r.qsubs {
for _, s := range v {
if string(s.subject) == subj {
continue FOR_DENY
}
}
}
for _, s := range r.psubs {
mergeDenyPerm(&c.perms.pub, denyPubs)
}
if what == sub || what == both {
if c.perms.sub.deny == nil {
// Avoid sublist cache contention in canSubscribe.
c.perms.sub.deny = NewSublistNoCache()
}
mergeDenyPerm(&c.perms.sub, denyPubs)
}
}
// mergeDenyPerm inserts new deny permissions, skipping subjects that already exist.
func mergeDenyPerm(p *perm, denyPubs []string) {
FOR_DENY:
for _, subj := range denyPubs {
r := p.deny.Match(subj)
for _, v := range r.qsubs {
for _, s := range v {
if string(s.subject) == subj {
continue FOR_DENY
}
}
sub := &subscription{subject: []byte(subj)}
p.deny.Insert(sub)
}
for _, s := range r.psubs {
if string(s.subject) == subj {
continue FOR_DENY
}
}
sub := &subscription{subject: []byte(subj)}
p.deny.Insert(sub)
}
}
@@ -1535,6 +1549,11 @@ func (c *client) readLoop(pre []byte) {
acc.stats.Unlock()
}
if c.kind == CLIENT {
atomic.AddInt64(&s.inClientMsgs, inMsgs)
atomic.AddInt64(&s.inClientBytes, inBytes)
}
atomic.AddInt64(&s.inMsgs, inMsgs)
atomic.AddInt64(&s.inBytes, inBytes)
}
@@ -2684,6 +2703,12 @@ func (c *client) processPing() {
srv.mu.Lock()
info := srv.copyInfo()
c.mu.Lock()
// Keep the in-process tls_required override from the initial INFO,
// otherwise this async INFO would flip it back to true.
if c.iproc && info.TLSRequired && !c.flags.isSet(didTLSFirst) {
info.TLSRequired = false
info.TLSAvailable = true
}
info.RemoteAccount = c.acc.Name
info.IsSystemAccount = c.acc == srv.SystemAccount()
info.ConnectInfo = true
@@ -3240,9 +3265,9 @@ func (c *client) addShadowSub(sub *subscription, ime *ime) (*subscription, error
return &nsub, nil
}
// canSubscribe determines if the client is authorized to subscribe to the
// given subject. Assumes caller is holding lock.
func (c *client) canSubscribe(subject string, optQueue ...string) bool {
// canSubscribeInternal determines if the client is authorized to subscribe to
// the given subject. Assumes caller is holding at least a read lock.
func (c *client) canSubscribeInternal(subject string, optQueue ...string) bool {
if c.perms == nil {
return true
}
@@ -3287,23 +3312,32 @@ func (c *client) canSubscribe(subject string, optQueue ...string) bool {
// If the queue appears in the deny list, then DO NOT allow.
allowed = !queueMatches(queue, r.qsubs)
}
}
return allowed
}
// We use the actual subscription to signal us to spin up the deny mperms
// and cache. We check if the subject is a wildcard that intersects any of
// the deny clauses.
// FIXME(dlc) - We could be smarter and track when these go away and remove.
if allowed && c.mperms == nil && subjectHasWildcard(subject) {
// Whip through the deny array and check if this wildcard subject can
// overlap with any denied deliveries.
for _, sub := range c.darray {
if SubjectsCollide(sub, subject) {
c.loadMsgDenyFilter()
break
}
// canSubscribe determines if the client is authorized to subscribe to the
// given subject and initializes the delivery-time deny filter when needed.
// Assumes caller is holding the write lock.
func (c *client) canSubscribe(subject string, optQueue ...string) bool {
if !c.canSubscribeInternal(subject, optQueue...) {
return false
}
// We use the actual subscription to signal us to spin up the deny mperms
// and cache. We check if the subject is a wildcard that intersects any of
// the deny clauses.
// FIXME(dlc) - We could be smarter and track when these go away and remove.
if c.mperms == nil && subjectHasWildcard(subject) {
// Whip through the deny array and check if this wildcard subject can
// overlap with any denied deliveries.
for _, sub := range c.darray {
if SubjectsCollide(sub, subject) {
c.loadMsgDenyFilter()
break
}
}
}
return allowed
return true
}
func queueMatches(queue string, qsubs [][]*subscription) bool {
@@ -4419,7 +4453,7 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
if c.srv.gateway.enabled {
reply := c.pa.reply
if len(c.pa.deliver) > 0 && c.kind == JETSTREAM && len(reply) > 0 && !replyHasJSAckSuffix(reply) {
reply = append(reply, '@')
reply = append(slices.Clip(reply), '@')
reply = append(reply, c.pa.deliver...)
}
didDeliver = c.sendMsgToGateways(acc, msg, c.pa.subject, reply, qnames, false) || didDeliver
@@ -4467,7 +4501,7 @@ func (c *client) handleGWReplyMap(msg []byte) bool {
if c.srv.gateway.enabled {
reply := c.pa.reply
if len(c.pa.deliver) > 0 && c.kind == JETSTREAM && len(reply) > 0 && !replyHasJSAckSuffix(reply) {
reply = append(reply, '@')
reply = append(slices.Clip(reply), '@')
reply = append(reply, c.pa.deliver...)
}
c.sendMsgToGateways(c.acc, msg, c.pa.subject, reply, nil, false)
@@ -4540,7 +4574,8 @@ func removeHeaderIfPrefixPresent(hdr []byte, prefix string) []byte {
}
index += start
if index < 1 || hdr[index-1] != '\n' {
return hdr
index += len(prefix)
continue
}
end := bytes.Index(hdr[index+len(prefix):], []byte(_CRLF_))
@@ -5119,6 +5154,7 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
var dlvExtraSize int64
var dlvRouteMsgs int64
var dlvLeafMsgs int64
var dlvClientMsgs int64
// We need to know if this is a MQTT producer because they send messages
// without CR_LF (we otherwise remove the size of CR_LF from message size).
@@ -5132,12 +5168,15 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
totalBytes := dlvMsgs*int64(len(msg)) + dlvExtraSize
routeBytes := dlvRouteMsgs*int64(len(msg)) + dlvExtraSize
leafBytes := dlvLeafMsgs*int64(len(msg)) + dlvExtraSize
// dlvExtraSize applies to route/leaf header overhead, not client deliveries
clientBytes := dlvClientMsgs * int64(len(msg))
// For non MQTT producers, remove the CR_LF * number of messages
if !prodIsMQTT {
totalBytes -= dlvMsgs * int64(LEN_CR_LF)
routeBytes -= dlvRouteMsgs * int64(LEN_CR_LF)
leafBytes -= dlvLeafMsgs * int64(LEN_CR_LF)
clientBytes -= dlvClientMsgs * int64(LEN_CR_LF)
}
if acc != nil {
@@ -5158,6 +5197,9 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
if srv := c.srv; srv != nil {
atomic.AddInt64(&srv.outMsgs, dlvMsgs)
atomic.AddInt64(&srv.outBytes, totalBytes)
atomic.AddInt64(&srv.outClientMsgs, dlvClientMsgs)
atomic.AddInt64(&srv.outClientBytes, clientBytes)
}
}
@@ -5253,6 +5295,9 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
// We don't count internal deliveries, so do only when sub.icb is nil.
if sub.icb == nil {
dlvMsgs++
if sub.client.kind == CLIENT {
dlvClientMsgs++
}
}
didDeliver = true
}
@@ -5480,6 +5525,8 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
dlvRouteMsgs++
case LEAF:
dlvLeafMsgs++
case CLIENT:
dlvClientMsgs++
}
}
// Do the rest even when message delivery was skipped.
@@ -5518,7 +5565,7 @@ sendToRoutesOrLeafs:
// already performed, otherwise we'd end up with a duplicate '@' suffix
// resulting in a protocol error.
if len(deliver) > 0 && len(reply) > 0 && !remapped && !replyHasJSAckSuffix(reply) {
reply = append(reply, '@')
reply = append(slices.Clip(reply), '@')
reply = append(reply, deliver...)
}
@@ -6538,10 +6585,20 @@ func (c *client) doTLSHandshake(typ string, solicit bool, url *url.URL, tlsConfi
if len(subjs) > 0 {
detail = fmt.Sprintf(" (%s)", strings.Join(subjs, "; "))
}
if kind == CLIENT {
c.Errorf("TLS handshake error: %v%s", err, detail)
} else {
if kind == ROUTER || kind == GATEWAY {
// Always surface these as errors, as these ports shouldn't be behind a load
// balancer or regularly probed.
c.Errorf("TLS %s handshake error: %v%s", typ, err, detail)
} else {
logf := c.Errorf
if isClientProbeTLSHandshakeError(err) {
logf = c.Debugf
}
if kind == CLIENT {
logf("TLS handshake error: %v%s", err, detail)
} else {
logf("TLS %s handshake error: %v%s", typ, err, detail)
}
}
c.closeConnection(TLSHandshakeError)
@@ -6571,6 +6628,17 @@ func (c *client) doTLSHandshake(typ string, solicit bool, url *url.URL, tlsConfi
return false, err
}
func isClientProbeTLSHandshakeError(err error) bool {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return true
}
var recordHeaderErr tls.RecordHeaderError
// Conn is only set by crypto/tls when the invalid record was the peer's
// initial handshake bytes, which is the non-TLS probe/load-balancer case.
return errors.As(err, &recordHeaderErr) && recordHeaderErr.Conn != nil
}
// getRawAuthUserLock returns the raw auth user for the client.
// Will acquire the client lock.
func (c *client) getRawAuthUserLock() string {
+1 -1
View File
@@ -66,7 +66,7 @@ func init() {
const (
// VERSION is the current version for the server.
VERSION = "2.14.0"
VERSION = "2.14.2"
// PROTO is the currently supported protocol.
// 0 was the original
+120 -45
View File
@@ -511,6 +511,7 @@ type consumer struct {
retention RetentionPolicy
monitorWg sync.WaitGroup
monitorMu sync.Mutex // Serializes monitorWg's Add against Wait to prevent a WaitGroup reuse panic.
inMonitor bool
// R>1 proposals
@@ -1140,7 +1141,10 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
mset.mu.Unlock()
return nil, NewJSConsumerWQRequiresExplicitAckError()
}
if config.DeliverPolicy != DeliverAll {
mset.mu.Unlock()
return nil, NewJSConsumerWQConsumerNotDeliverAllError()
}
if mset.numLimitableConsumers() > 0 {
subjects := gatherSubjectFilters(config.FilterSubject, config.FilterSubjects)
if len(subjects) == 0 {
@@ -1168,10 +1172,6 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
}
}
}
if config.DeliverPolicy != DeliverAll {
mset.mu.Unlock()
return nil, NewJSConsumerWQConsumerNotDeliverAllError()
}
}
// Set name, which will be durable name if set, otherwise we create one at random.
@@ -1336,6 +1336,8 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
// Clustered non-direct consumers defer this to setLeader so the
// expensive store scans don't block the meta apply goroutine.
if err := o.selectStartingSeqNo(); err != nil {
mset.mu.Unlock()
o.deleteWithoutAdvisory()
return nil, err
}
}
@@ -1832,11 +1834,14 @@ func (o *consumer) setLeader(isLeader bool) error {
stopAndClearTimer(&o.uptmr)
// Make sure to clear out any re-deliver queues
o.stopAndClearPtmr()
o.rdc = nil
o.rdq = nil
o.rdqi.Empty()
o.pending = nil
o.rsm = nil
o.resetPendingDeliveries()
// Reset num pending, these are only authoritative on the leader.
o.npc, o.npf = 0, 0
// ok if they are nil, we protect inside unsubscribe()
o.unsubscribe(o.ackSubOld)
o.unsubscribe(o.ackSub)
@@ -2138,7 +2143,7 @@ func (o *consumer) deleteNotActive() {
cnaStart := consumerNotActiveStartInterval
o.mu.Lock()
if o.mset == nil {
if o.mset == nil || !o.isLeader() {
o.mu.Unlock()
return
}
@@ -2213,6 +2218,8 @@ func (o *consumer) deleteNotActive() {
s, js := o.mset.srv, o.srv.js.Load()
acc, stream, name, isDirect := o.acc.Name, o.stream, o.name, o.cfg.Direct
// Capture our own view of the assignment while we still hold the lock.
ca := o.ca
var qch, cqch chan struct{}
if o.srv != nil {
qch = o.srv.quitCh
@@ -2230,9 +2237,6 @@ func (o *consumer) deleteNotActive() {
"consumer": name,
})
// We will delete locally regardless.
defer o.delete()
// If we are clustered, check if we still have this consumer assigned.
// If we do forward a proposal to delete ourselves to the metacontroller leader.
if !isDirect && s.JetStreamIsClustered() {
@@ -2241,8 +2245,11 @@ func (o *consumer) deleteNotActive() {
meta RaftNode
removeEntry []byte
)
ca, cc := js.consumerAssignment(acc, stream, name), js.cluster
if ca != nil && cc != nil {
nca := js.consumerAssignment(acc, stream, name)
// Only propose the delete if the meta-layer assignment still refers to
// the consumer we captured, otherwise we'd be racing a recreated
// consumer with the same name.
if cc := js.cluster; cc != nil && ca != nil && ca.sameIdentity(nca) {
meta = cc.meta
cca := ca.clone()
cca.Reply = _EMPTY_
@@ -2251,7 +2258,7 @@ func (o *consumer) deleteNotActive() {
}
js.mu.RUnlock()
if ca != nil && cc != nil {
if ca != nil && meta != nil {
// Check to make sure we went away.
// Don't think this needs to be a monitored go routine.
jitter := time.Duration(rand.Int63n(int64(cnaStart)))
@@ -2274,10 +2281,11 @@ func (o *consumer) deleteNotActive() {
js.mu.RUnlock()
return
}
nca := js.consumerAssignment(acc, stream, name)
js.mu.RUnlock()
nca = js.consumerAssignment(acc, stream, name)
// Make sure this is the same consumer assignment, and not a new consumer with the same name.
if nca != nil && reflect.DeepEqual(nca, ca) {
match := ca.sameIdentity(nca)
js.mu.RUnlock()
if match {
s.Warnf("Consumer assignment for '%s > %s > %s' not cleaned up, retrying", acc, stream, name)
meta.ForwardProposal(removeEntry)
if interval < cnaMax {
@@ -2290,6 +2298,10 @@ func (o *consumer) deleteNotActive() {
return
}
}
} else {
// Otherwise, we can delete locally. Either a consumer that's not tracked
// by the meta layer (direct), or a standalone non-clustered server.
o.delete()
}
}
@@ -2341,7 +2353,9 @@ func (o *consumer) hasMaxDeliveries(seq uint64) bool {
// Make sure to remove from pending.
if p, ok := o.pending[seq]; ok && p != nil {
delete(o.pending, seq)
o.updateDelivered(p.Sequence, seq, dc, p.Timestamp)
// Increment by one, since the delivery count hasn't been increased above.
o.updateDelivered(p.Sequence, seq, dc+1, p.Timestamp)
o.moveAckFloor(p.Sequence, seq)
}
// Ensure redelivered state is set, if not already.
if o.rdc == nil {
@@ -3245,14 +3259,42 @@ func (o *consumer) ackWait(next time.Duration) time.Duration {
return o.cfg.AckWait + ackWaitDelay
}
// Due to bug in calculation of sequences on restoring redelivered let's do quick sanity check.
// Lock should be held.
func (o *consumer) checkRedelivered() {
var shouldUpdateState bool
func (o *consumer) removeRedeliveredBelow(seq uint64) {
if seq == 0 {
return
}
o.mu.Lock()
for sseq := range o.rdc {
if sseq <= o.asflr {
if sseq < seq {
delete(o.rdc, sseq)
o.removeFromRedeliverQueue(sseq)
}
}
o.mu.Unlock()
if o.store != nil {
o.store.RemoveRedeliveredBelow(seq)
}
}
// checkRedelivered drops rdq entries at/below asflr or below stream's first sequence.
// But rdc is kept until the message leaves the stream: needAck relies on rdc to mark
// messages past MaxDeliver.
// Lock should be held.
func (o *consumer) checkRedelivered() {
if o.mset == nil {
return
}
var ss StreamState
o.mset.store.FastState(&ss)
var shouldUpdateState bool
for sseq := range o.rdc {
if sseq <= o.asflr || sseq < ss.FirstSeq {
o.removeFromRedeliverQueue(sseq)
}
if sseq < ss.FirstSeq {
delete(o.rdc, sseq)
shouldUpdateState = true
}
}
@@ -3641,22 +3683,7 @@ func (o *consumer) processAckMsgLocked(sseq, dseq, dc uint64, reply string, doSa
delete(o.pending, sseq)
// Use the original deliver sequence from our pending record.
dseq = p.Sequence
// Only move floors if we matched an existing pending.
if len(o.pending) == 0 {
o.adflr = o.dseq - 1
o.asflr = o.sseq - 1
} else if dseq == o.adflr+1 {
o.adflr, o.asflr = dseq, sseq
for ss := sseq + 1; ss < o.sseq; ss++ {
if p, ok := o.pending[ss]; ok {
if p.Sequence > 0 {
o.adflr, o.asflr = p.Sequence-1, ss-1
}
break
}
}
}
o.moveAckFloor(dseq, sseq)
}
delete(o.rdc, sseq)
o.removeFromRedeliverQueue(sseq)
@@ -3727,6 +3754,25 @@ func (o *consumer) processAckMsgLocked(sseq, dseq, dc uint64, reply string, doSa
return ackInPlace
}
// Lock should be held.
func (o *consumer) moveAckFloor(dseq, sseq uint64) {
// Only move floors if we matched an existing pending.
if len(o.pending) == 0 {
o.adflr = o.dseq - 1
o.asflr = o.sseq - 1
} else if dseq == o.adflr+1 {
o.adflr, o.asflr = dseq, sseq
for ss := sseq + 1; ss < o.sseq; ss++ {
if p, ok := o.pending[ss]; ok {
if p.Sequence > 0 {
o.adflr, o.asflr = p.Sequence-1, ss-1
}
break
}
}
}
}
// Determine if this is a truly filtered consumer. Modern clients will place filtered subjects
// even if the stream only has a single non-wildcard subject designation.
// Read lock should be held.
@@ -4763,7 +4809,9 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) {
// Make sure to remove from pending.
if p, ok := o.pending[seq]; ok && p != nil {
delete(o.pending, seq)
// The delivery count has already been incremented once.
o.updateDelivered(p.Sequence, seq, dc, p.Timestamp)
o.moveAckFloor(p.Sequence, seq)
}
continue
}
@@ -5507,11 +5555,11 @@ func (o *consumer) streamNumPendingLocked() (uint64, error) {
return o.streamNumPending()
}
// Will force a set from the stream store of num pending.
// Will force a set from the stream store of num pending on the consumer leader.
// Depends on delivery policy, for last per subject we calculate differently.
// Lock should be held.
func (o *consumer) streamNumPending() (uint64, error) {
if o.mset == nil || o.mset.store == nil {
if o.mset == nil || o.mset.store == nil || !o.isLeader() {
o.npc, o.npf = 0, 0
return 0, nil
}
@@ -6252,7 +6300,10 @@ func (o *consumer) selectStartingSeqNo() error {
o.asflr = o.sseq - 1
// Set our starting sequence state.
// But only if we're not clustered, if clustered we propose upon becoming leader.
if o.store != nil && o.sseq > 0 && o.cfg.replicas(&o.mset.cfg) == 1 {
o.mset.cfgMu.RLock()
isR1 := o.cfg.replicas(&o.mset.cfg) == 1
o.mset.cfgMu.RUnlock()
if o.store != nil && o.sseq > 0 && isR1 {
if err := o.store.SetStarting(o.sseq - 1); err != nil {
return err
}
@@ -6405,9 +6456,9 @@ func (o *consumer) purge(sseq uint64, slseq uint64, isWider bool) {
}
delete(o.pending, seq)
delete(o.rdc, seq)
o.updateAcks(p.Sequence, seq, _EMPTY_)
// rdq handled below.
}
if isWider && store != nil {
} else if isWider && store != nil {
// Our filtered subject, which could be all, is wider than the underlying purge.
// We need to check if the pending items left are still valid.
var smv StoreMsg
@@ -6420,6 +6471,7 @@ func (o *consumer) purge(sseq uint64, slseq uint64, isWider bool) {
}
delete(o.pending, seq)
delete(o.rdc, seq)
o.updateAcks(p.Sequence, seq, _EMPTY_)
}
}
}
@@ -6778,6 +6830,10 @@ func (o *consumer) decStreamPending(sseq uint64, subj string) {
var rdc uint64
if wasPending {
rdc = o.deliveryCount(sseq)
} else if _, ok := o.rdc[sseq]; ok && o.isLeader() {
delete(o.rdc, sseq)
// Pass 0 as the delivered sequence to only remove the redelivered state.
o.updateAcks(0, sseq, _EMPTY_)
}
o.mu.Unlock()
@@ -6878,14 +6934,21 @@ func gatherSubjectFilters(filter string, filters []string) []string {
// shouldStartMonitor will return true if we should start a monitor
// goroutine or will return false if one is already running.
func (o *consumer) shouldStartMonitor() bool {
o.mu.Lock()
defer o.mu.Unlock()
// monitorMu is held across the monitorWg.Add below so that it cannot race
// a concurrent monitorWg.Wait in stopMonitoring. It is taken before o.mu to
// keep a consistent lock ordering.
o.monitorMu.Lock()
defer o.monitorMu.Unlock()
o.mu.Lock()
if o.inMonitor {
o.mu.Unlock()
return false
}
o.monitorWg.Add(1)
o.inMonitor = true
o.mu.Unlock()
o.monitorWg.Add(1)
return true
}
@@ -6901,6 +6964,18 @@ func (o *consumer) clearMonitorRunning() {
}
}
// stopMonitoring signals any running monitor goroutine to quit and waits for
// it to fully exit.
func (o *consumer) stopMonitoring() {
// monitorMu is held across both the quit signal and the wait so that a
// concurrent shouldStartMonitor cannot slip a new monitor generation in
// between.
o.monitorMu.Lock()
defer o.monitorMu.Unlock()
o.signalMonitorQuit()
o.monitorWg.Wait()
}
// Test whether we are in the monitor routine.
func (o *consumer) isMonitorRunning() bool {
o.mu.RLock()
+1 -1
View File
@@ -230,7 +230,7 @@ func (store *DirJWTStore) Pack(maxJWTs int) (string, error) {
}
store.Lock()
err := filepath.Walk(store.directory, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && strings.HasSuffix(path, fileExtension) { // this is a JWT
if info != nil && !info.IsDir() && strings.HasSuffix(path, fileExtension) { // this is a JWT
if count == maxJWTs { // won't match negative
return nil
}
+11 -1
View File
@@ -374,7 +374,9 @@ type ServerStats struct {
ActiveAccounts int `json:"active_accounts"`
NumSubs uint32 `json:"subscriptions"`
Sent DataStats `json:"sent"`
SentToClients DataStats `json:"sent_to_clients"`
Received DataStats `json:"received"`
ReceivedFromClients DataStats `json:"received_from_clients"`
SlowConsumers int64 `json:"slow_consumers"`
SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats,omitempty"`
StaleConnections int64 `json:"stale_connections,omitempty"`
@@ -612,7 +614,7 @@ RESET:
// Optional raw header addition.
if pm.hdr != nil {
b = append(pm.hdr, b...)
b = append(pm.hdr[:len(pm.hdr):len(pm.hdr)], b...)
nhdr := len(pm.hdr)
nsize := len(b) - LEN_CR_LF
// MQTT producers don't have CRLF, so add it back.
@@ -948,8 +950,12 @@ func (s *Server) sendStatsz(subj string) {
m.Stats.ActiveAccounts = int(atomic.LoadInt32(&s.activeAccounts))
m.Stats.Received.Msgs = atomic.LoadInt64(&s.inMsgs)
m.Stats.Received.Bytes = atomic.LoadInt64(&s.inBytes)
m.Stats.ReceivedFromClients.Msgs = atomic.LoadInt64(&s.inClientMsgs)
m.Stats.ReceivedFromClients.Bytes = atomic.LoadInt64(&s.inClientBytes)
m.Stats.Sent.Msgs = atomic.LoadInt64(&s.outMsgs)
m.Stats.Sent.Bytes = atomic.LoadInt64(&s.outBytes)
m.Stats.SentToClients.Msgs = atomic.LoadInt64(&s.outClientMsgs)
m.Stats.SentToClients.Bytes = atomic.LoadInt64(&s.outClientBytes)
m.Stats.SlowConsumers = atomic.LoadInt64(&s.slowConsumers)
// Evaluate the slow consumer stats, but set it only if one of the value is not 0.
scs := &SlowConsumersStats{
@@ -1880,6 +1886,10 @@ func (s *Server) shutdownEventing() {
}
s.mu.Lock()
if s.sys == nil || s.sys.resetCh == nil {
s.mu.Unlock()
return
}
clearTimer(&s.sys.sweeper)
clearTimer(&s.sys.stmr)
rc := s.sys.resetCh
+210 -105
View File
@@ -377,6 +377,10 @@ const (
rlBadThresh = 32 * 1024 * 1024
// Checksum size for hash for msg records.
recordHashSize = 8
// Above this number of subjects, index.db may not be written regularly anymore, and
// certain psim optimisations may not be used.
highCardinalityThreshold = 1_000_000
)
func newFileStore(fcfg FileStoreConfig, cfg StreamConfig) (*fileStore, error) {
@@ -739,7 +743,7 @@ func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error {
fs.ageChkTime = 0
}
if fs.cfg.MaxMsgsPer > 0 && (old_cfg.MaxMsgsPer == 0 || fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer) {
if fs.cfg.MaxMsgsPer > 0 && (old_cfg.MaxMsgsPer <= 0 || fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer) {
if err := fs.enforceMsgPerSubjectLimit(true); err != nil {
fs.mu.Unlock()
return err
@@ -1425,6 +1429,7 @@ func (mb *msgBlock) convertCipher() error {
// Reset the cache since we just read everything in.
mb.cache = nil
mb.ecache.Set(nil)
// Generate new keys. If we error for some reason then we will put
// the old keyfile back.
@@ -1462,10 +1467,19 @@ func (mb *msgBlock) convertToEncrypted() error {
} else if err = mb.indexCacheBuf(buf); err != nil {
// This likely indicates this was already encrypted or corrupt.
mb.cache = nil
mb.ecache.Set(nil)
return err
}
// Undo cache from above for later.
mb.cache = nil
mb.ecache.Set(nil)
// Regenerate mb.bek so that the keystream offset is at zero. This matches
// what encryptOrDecryptIfNeeded does on read-back, otherwise re-entering
// convertToEncrypted with a previously-used mb.bek would write ciphertext at
// the wrong stream offset and silently corrupt the block.
if mb.bek, err = genBlockEncryptionKey(mb.fs.fcfg.Cipher, mb.seed, mb.nonce); err != nil {
return err
}
mb.bek.XORKeyStream(buf, buf)
<-dios
err = os.WriteFile(mb.mfn, buf, defaultFilePerms)
@@ -2208,18 +2222,22 @@ func (fs *fileStore) recoverTTLState() error {
// Done.
break
}
msg, _, err := mb.fetchMsgNoCopy(seq, &sm)
mb.mu.Lock()
msg, _, err := mb.fetchMsgNoCopyLocked(seq, &sm)
if err != nil {
mb.finishedWithCache()
mb.mu.Unlock()
fs.warn("Error loading msg seq %d for recovering TTL: %s", seq, err)
continue
}
if len(msg.hdr) == 0 {
continue
}
if ttl, _ := getMessageTTL(msg.hdr); ttl > 0 {
expires := time.Duration(msg.ts) + (time.Second * time.Duration(ttl))
fs.ttls.Add(seq, int64(expires))
if len(msg.hdr) > 0 {
if ttl, _ := getMessageTTL(msg.hdr); ttl > 0 {
expires := time.Duration(msg.ts) + (time.Second * time.Duration(ttl))
fs.ttls.Add(seq, int64(expires))
}
}
mb.finishedWithCache()
mb.mu.Unlock()
}
}
return nil
@@ -2289,18 +2307,22 @@ func (fs *fileStore) recoverMsgSchedulingState() error {
// Done.
break
}
msg, _, err := mb.fetchMsgNoCopy(seq, &sm)
mb.mu.Lock()
msg, _, err := mb.fetchMsgNoCopyLocked(seq, &sm)
if err != nil {
mb.finishedWithCache()
mb.mu.Unlock()
fs.warn("Error loading msg seq %d for recovering message schedules: %s", seq, err)
continue
}
if len(msg.hdr) == 0 {
continue
}
if schedule, apiErr := nextMessageSchedule(sm.hdr, sm.ts); apiErr == nil && !schedule.IsZero() {
// Copy the subject, as it's stored in the scheduling maps and the backing cache could be reused in the meantime.
fs.scheduling.init(seq, copyString(sm.subj), schedule.UnixNano())
if len(msg.hdr) > 0 {
if schedule, apiErr := nextMessageSchedule(msg.hdr, msg.ts); apiErr == nil && !schedule.IsZero() {
// Copy the subject, as it's stored in the scheduling maps and the backing cache could be reused in the meantime.
fs.scheduling.init(seq, copyString(msg.subj), schedule.UnixNano())
}
}
mb.finishedWithCache()
mb.mu.Unlock()
}
}
return nil
@@ -2755,6 +2777,7 @@ func (fs *fileStore) GetSeqFromTime(t time.Time) uint64 {
// Using a binary search, but need to be aware of interior deletes in the block.
seq := lseq + 1
mb.mu.Lock()
loop:
for fseq <= lseq {
mid := fseq + (lseq-fseq)/2
@@ -2762,7 +2785,7 @@ loop:
// Potentially skip over gaps. We keep the original middle but keep track of a
// potential delete range with an offset.
for {
sm, _, err := mb.fetchMsgNoCopy(mid+off, &smv)
sm, _, err := mb.fetchMsgNoCopyLocked(mid+off, &smv)
if err != nil || sm == nil {
off++
if mid+off <= lseq {
@@ -2789,6 +2812,8 @@ loop:
fseq = mid + off + 1
}
}
mb.finishedWithCache()
mb.mu.Unlock()
return seq
}
@@ -2829,14 +2854,11 @@ func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *
// If there are no subject matches then this is effectively no-op.
hseq := uint64(math.MaxUint64)
var ierr error
stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) {
if ierr != nil {
return
}
stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) bool {
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
// mb is already loaded into the cache so should be fast-ish.
if ierr = mb.recalculateForSubj(bytesToString(subj), ss); ierr != nil {
return
return false
}
}
first := max(start, ss.First)
@@ -2844,12 +2866,12 @@ func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *
// The start cutoff is after the last sequence for this subject,
// or we think we already know of a subject with an earlier msg
// than our first seq for this subject.
return
return true
}
// Need messages loaded from here on out.
if mb.cacheNotLoaded() {
if ierr = mb.loadMsgsWithLock(); ierr != nil {
return
return false
}
didLoad = true
}
@@ -2863,7 +2885,7 @@ func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *
sm = fsm
hseq = ss.First
}
return
return true
}
for seq := first; seq <= ss.Last; seq++ {
// Otherwise we have a start floor that intersects where this subject
@@ -2889,6 +2911,7 @@ func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *
// If we are here we did not match, so put the llseq back.
mb.llseq = llseq
}
return true
})
if ierr != nil {
return nil, false, ierr
@@ -3126,14 +3149,11 @@ func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *S
// If there are no subject matches then this is effectively no-op.
hseq := uint64(0)
var ierr error
stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) {
if ierr != nil {
return
}
stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) bool {
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
// mb is already loaded into the cache so should be fast-ish.
if ierr = mb.recalculateForSubj(bytesToString(subj), ss); ierr != nil {
return
return false
}
}
first := min(start, ss.Last)
@@ -3142,7 +3162,7 @@ func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *S
if first < ss.First || first <= hseq {
// The start cutoff is before the first sequence for this subject,
// or we already know of a subject with a later-or-equal msg.
return
return true
}
if first == ss.Last {
// If the start floor is above where this subject starts then we can
@@ -3151,7 +3171,7 @@ func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *S
sm = fsm
hseq = ss.Last
}
return
return true
}
for seq := first; seq >= ss.First; seq-- {
// Otherwise we have a start floor that intersects where this subject
@@ -3177,6 +3197,7 @@ func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *S
// If we are here we did not match, so put the llseq back.
mb.llseq = llseq
}
return true
})
if ierr != nil {
return nil, false, ierr
@@ -3417,6 +3438,9 @@ func (fs *fileStore) checkSkipFirstBlock(filter string, wc bool, bi int) (int, e
// Move through psim to gather start and stop bounds.
start, stop := uint32(math.MaxUint32), uint32(0)
if wc {
if fs.psim.Size() > highCardinalityThreshold {
return bi + 1, nil
}
fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) {
if psi.fblk < start {
start = psi.fblk
@@ -3438,16 +3462,29 @@ func (fs *fileStore) checkSkipFirstBlock(filter string, wc bool, bi int) (int, e
// This is used to see if we can selectively jump start blocks based on filter subjects and a starting block index.
// Will return -1 and ErrStoreEOF if no matches at all or no more from where we are.
func (fs *fileStore) checkSkipFirstBlockMulti(sl *gsl.SimpleSublist, bi int) (int, error) {
// Don't bother if full wildcard.
if sl.MatchesFullWildcard() || fs.psim.Size() > highCardinalityThreshold {
return bi + 1, nil
}
// Move through psim to gather start and stop bounds.
start, stop := uint32(math.MaxUint32), uint32(0)
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
guard := fs.blks[bi].getIndex() + 1
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool {
if psi.fblk < start {
start = psi.fblk
}
if start == guard {
// One of the subjects matches the next block, so there's no point in carrying on trying to skip.
return false
}
if psi.lblk > stop {
stop = psi.lblk
}
return true
})
if start == guard {
return bi + 1, nil
}
// Nothing was found.
if start == uint32(math.MaxUint32) {
return -1, ErrStoreEOF
@@ -4322,10 +4359,10 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
mb := fs.blks[seqStart]
bi := mb.index
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool {
// If the select blk start is greater than entry's last blk skip.
if bi > psi.lblk {
return
return true
}
total++
// We will track the subjects that are an exact match to the last block.
@@ -4333,6 +4370,7 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
if psi.lblk == bi {
lbm[string(subj)] = true
}
return true
})
// Now check if we need to inspect the seqStart block.
@@ -4422,18 +4460,11 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
var ierr error
var havePartial bool
var updateLLTS bool
stree.IntersectGSL[SimpleState](mb.fss, sl, func(bsubj []byte, ss *SimpleState) {
if ierr != nil {
return
}
stree.IntersectGSL[SimpleState](mb.fss, sl, func(bsubj []byte, ss *SimpleState) bool {
subj := bytesToString(bsubj)
if havePartial {
// If we already found a partial then don't do anything else.
return
}
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
if ierr = mb.recalculateForSubj(subj, ss); ierr != nil {
return
return false
}
}
if sseq <= ss.First {
@@ -4441,7 +4472,9 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
} else if sseq <= ss.Last {
// We matched but its a partial.
havePartial = true
return false
}
return true
})
if ierr != nil {
mb.mu.Unlock()
@@ -4494,12 +4527,13 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
// If we are here it's better to calculate totals from psim and adjust downward by scanning less blocks.
start := uint32(math.MaxUint32)
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool {
total += psi.total
// Keep track of start index for this subject.
if psi.fblk < start {
start = psi.fblk
}
return true
})
// See if we were asked for all, if so we are done.
@@ -4545,8 +4579,9 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
stree.IntersectGSL(mb.fss, sl, func(bsubj []byte, ss *SimpleState) {
stree.IntersectGSL(mb.fss, sl, func(bsubj []byte, ss *SimpleState) bool {
adjust += ss.Msgs
return true
})
}
} else {
@@ -4716,6 +4751,7 @@ func (fs *fileStore) newMsgBlockForWrite() (*msgBlock, error) {
}
// If we had a write error before, don't allow continuing into a new block.
if err := lmb.werr; err != nil {
lmb.mu.Unlock()
return nil, err
}
// Flush any pending messages.
@@ -4894,15 +4930,17 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t
}
// Adjust top level tracking of per subject msg counts.
var info *psi
var ok bool
if len(subj) > 0 && fs.psim != nil {
index := fs.lmb.index
if info, ok := fs.psim.Find(stringToBytes(subj)); ok {
if info, ok = fs.psim.Find(stringToBytes(subj)); ok {
info.total++
if index > info.lblk {
info.lblk = index
}
} else {
fs.psim.Insert(stringToBytes(subj), psi{total: 1, fblk: index, lblk: index})
info, _ = fs.psim.Insert(stringToBytes(subj), psi{total: 1, fblk: index, lblk: index})
fs.tsl += len(subj)
}
}
@@ -4957,6 +4995,10 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t
}
}
}
// If we only ever store one/last message for a subject, can correct the first block to where we've just written.
if info != nil && info.total == 1 && mmp == 1 {
info.fblk = info.lblk
}
// Limits checks and enforcement.
// If they do any deletions they will update the
@@ -5728,7 +5770,6 @@ func (fs *fileStore) removeMsgFromBlock(mb *msgBlock, seq uint64, secure, viaLim
lhdr, lmsg int
ttl int64
)
// We don't use a copy as long as that's possible. When unlocking mb or erasing, we'll copy the subject.
sm, err := mb.cacheLookupNoCopy(seq, &smv)
if err != nil {
finishedWithCache()
@@ -5739,7 +5780,9 @@ func (fs *fileStore) removeMsgFromBlock(mb *msgBlock, seq uint64, secure, viaLim
}
return false, err
} else if sm != nil {
subj = sm.subj
// subj aliases mb.cache.buf; copy now because the cache may be erased or
// recycled after we drop mb.mu. The rest are scalars stashed for later use.
subj = copyString(sm.subj)
ts = sm.ts
lhdr = len(sm.hdr)
lmsg = len(sm.msg)
@@ -5751,8 +5794,6 @@ func (fs *fileStore) removeMsgFromBlock(mb *msgBlock, seq uint64, secure, viaLim
// when the last block is empty.
// If not via limits and not empty (empty writes tombstone below if last) write tombstone.
if !viaLimits && !isEmpty && sm != nil {
// Need to copy the subject since we unlock and re-acquire, and the cache could change.
subj = copyString(subj)
mb.mu.Unlock() // Only safe way to checkLastBlock is to unlock here...
lmb, err := fs.checkLastBlock(emptyRecordLen)
if err != nil {
@@ -5785,9 +5826,6 @@ func (fs *fileStore) removeMsgFromBlock(mb *msgBlock, seq uint64, secure, viaLim
mb.mu.Unlock()
return false, err
}
// Need to copy the subject, as eraseMsg will overwrite the cache and we won't
// be able to access sm.subj anymore later on.
subj = copyString(subj)
if err := mb.eraseMsg(seq, int(ri), int(msz), isLastBlock); err != nil {
finishedWithCache()
mb.mu.Unlock()
@@ -6495,10 +6533,9 @@ func (mb *msgBlock) selectNextFirst() {
var smv StoreMsg
sm, _ := mb.cacheLookupNoCopy(seq, &smv)
if sm == nil {
// Slow path, need to unlock.
mb.mu.Unlock()
sm, _, _ = mb.fetchMsgNoCopy(seq, &smv)
mb.mu.Lock()
// Slow path, cache not loaded.
sm, _, _ = mb.fetchMsgNoCopyLocked(seq, &smv)
mb.finishedWithCache()
}
if sm != nil {
mb.first.ts = sm.ts
@@ -6676,8 +6713,14 @@ func (mb *msgBlock) tryExpireCacheLocked() {
}
// Check for activity on the cache that would prevent us from expiring.
if tns-bufts <= int64(mb.cexp) {
mb.resetCacheExpireTimer(mb.cexp - time.Duration(tns-bufts))
// Both tns and bufts come from ats.AccessTime(), which means bufts can understate
// how recent the last activity actually was by up to one tick.
if delta := tns - bufts; delta <= int64(mb.cexp)+int64(ats.TickInterval) {
td := mb.cexp - time.Duration(delta)
if td <= 0 {
td = ats.TickInterval
}
mb.resetCacheExpireTimer(td)
if strengthened {
mb.finishedWithCache()
}
@@ -7467,6 +7510,11 @@ func (fs *fileStore) writeTombstoneNoFlush(seq uint64, ts int64) error {
// Lock should be held.
func (mb *msgBlock) recompressOnDiskIfNeeded() error {
// If the block has been closed in the meantime, skip.
if mb.closed {
return nil
}
alg := mb.fs.fcfg.Compression
// Open up the file block and read in the entire contents into memory.
@@ -8459,25 +8507,25 @@ checkCache:
// We assume the block was selected and is correct, so we do not do range checks.
// Lock should not be held.
func (mb *msgBlock) fetchMsg(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
mb.mu.Lock()
defer mb.mu.Unlock()
defer mb.finishedWithCache()
return mb.fetchMsgEx(seq, sm, true)
}
// Fetch a message from this block, possibly reading in and caching the messages.
// We assume the block was selected and is correct, so we do not do range checks.
// We will not copy the msg data.
// Lock should not be held.
func (mb *msgBlock) fetchMsgNoCopy(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
// We will not copy the msg data, the returned StoreMsg's subj/hdr/msg/buf are aliased
// into mb.cache.buf and are only safe to read while mb.mu is held.
func (mb *msgBlock) fetchMsgNoCopyLocked(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
return mb.fetchMsgEx(seq, sm, false)
}
// Fetch a message from this block, possibly reading in and caching the messages.
// We assume the block was selected and is correct, so we do not do range checks.
// We will copy the msg data based on doCopy boolean.
// Lock should not be held.
// Lock should be held.
func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg, bool, error) {
mb.mu.Lock()
defer mb.mu.Unlock()
fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
if seq < fseq || seq > lseq {
return nil, false, ErrStoreMsgNotFound
@@ -8499,7 +8547,6 @@ func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg
return nil, false, err
}
}
defer mb.finishedWithCache()
llseq := mb.llseq
fsm, err := mb.cacheLookupEx(seq, sm, doCopy)
@@ -8507,7 +8554,7 @@ func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg
return nil, false, err
}
expireOk := (seq == lseq && llseq == seq-1) || (seq == fseq && llseq == seq+1)
return fsm, expireOk, err
return fsm, expireOk, nil
}
var (
@@ -8675,9 +8722,15 @@ func (fs *fileStore) sizeForSeq(seq uint64) int {
}
var smv StoreMsg
if mb := fs.selectMsgBlock(seq); mb != nil {
if sm, _, _ := mb.fetchMsgNoCopy(seq, &smv); sm != nil {
return int(fileStoreMsgSize(sm.subj, sm.hdr, sm.msg))
mb.mu.Lock()
sm, _, _ := mb.fetchMsgNoCopyLocked(seq, &smv)
var sz int
if sm != nil {
sz = int(fileStoreMsgSize(sm.subj, sm.hdr, sm.msg))
}
mb.finishedWithCache()
mb.mu.Unlock()
return sz
}
return 0
}
@@ -8867,9 +8920,17 @@ func (fs *fileStore) SubjectForSeq(seq uint64) (string, error) {
mb := fs.selectMsgBlock(seq)
fs.mu.RUnlock()
if mb != nil {
if sm, _, _ := mb.fetchMsgNoCopy(seq, &smv); sm != nil {
mb.mu.Lock()
sm, _, _ := mb.fetchMsgNoCopyLocked(seq, &smv)
var subj string
if sm != nil {
// Copy the subject, as it's used elsewhere, and the backing cache could be reused in the meantime.
return copyString(sm.subj), nil
subj = copyString(sm.subj)
}
mb.finishedWithCache()
mb.mu.Unlock()
if sm != nil {
return subj, nil
}
}
return _EMPTY_, ErrStoreMsgNotFound
@@ -8950,10 +9011,8 @@ func (fs *fileStore) loadLastLocked(subj string, sm *StoreMsg) (lsm *StoreMsg, e
if ss.lastNeedsUpdate {
// mb is already loaded into the cache so should be fast-ish.
if err = mb.recalculateForSubj(subj, ss); err != nil {
if err != nil {
mb.mu.Unlock()
return nil, err
}
mb.mu.Unlock()
return nil, err
}
}
l = ss.Last
@@ -9028,12 +9087,13 @@ func (fs *fileStore) LoadNextMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *
if start <= fs.state.FirstSeq {
var total uint64
blkStart := uint32(math.MaxUint32)
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool {
total += psi.total
// Keep track of start index for this subject.
if psi.fblk < blkStart {
blkStart = psi.fblk
}
return true
})
// Nothing available.
if total == 0 {
@@ -9718,6 +9778,10 @@ func compareFn(subject string) func(string, string) bool {
// PurgeEx will remove messages based on subject filters, sequence and number of messages to keep.
// Will return the number of purged messages.
func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) {
// sequence == 1 means "purge up to but not including 1", a no-op.
if sequence == 1 {
return 0, nil
}
if subject == _EMPTY_ || subject == fwcs {
if keep == 0 && sequence == 0 {
return fs.purge(0)
@@ -9725,6 +9789,10 @@ func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint
if sequence > 1 {
return fs.compact(sequence)
}
// Make sure to not leave subject if empty.
if subject == _EMPTY_ {
subject = fwcs
}
}
// Persist any write errors.
@@ -9736,11 +9804,6 @@ func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint
}
}()
// Make sure to not leave subject if empty and we reach this spot.
if subject == _EMPTY_ {
subject = fwcs
}
eq, wc := compareFn(subject), subjectHasWildcard(subject)
var firstSeqNeedsUpdate bool
var bytes uint64
@@ -9828,7 +9891,10 @@ func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint
continue
}
if sequence > 1 && sequence <= l {
// "Purge up to but not including sequence": sequence == 0 means no
// sequence filter; sequence >= 1 clamps the per-block upper bound to
// sequence-1 (so sequence == 1 leaves nothing to process).
if sequence >= 1 && sequence <= l {
l = sequence - 1
}
@@ -10018,7 +10084,11 @@ func (fs *fileStore) purge(fseq uint64) (purged uint64, rerr error) {
fs.state.Msgs = 0
for _, mb := range fs.blks {
mb.dirtyClose()
// These blocks are being discarded by the purge, so mark them closed.
mb.mu.Lock()
mb.dirtyCloseWithRemove(false)
mb.closed = true
mb.mu.Unlock()
}
// Check if we need to set the first seq to a new number.
@@ -10690,11 +10760,18 @@ func (fs *fileStore) Truncate(seq uint64) (rerr error) {
// at the end, after we release the lock.
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
var err error
var lsm *StoreMsg
var hasLsm bool
var lastTime int64
smb := fs.selectMsgBlock(seq)
if smb != nil {
lsm, _, err = smb.fetchMsgNoCopy(seq, nil)
smb.mu.Lock()
lsm, _, err := smb.fetchMsgNoCopyLocked(seq, nil)
if lsm != nil {
hasLsm = true
lastTime = lsm.ts
}
smb.finishedWithCache()
smb.mu.Unlock()
if err != nil && err != ErrStoreMsgNotFound && err != errDeletedMsg {
fs.mu.Unlock()
return err
@@ -10702,13 +10779,12 @@ func (fs *fileStore) Truncate(seq uint64) (rerr error) {
}
// Reset last so new block doesn't contain truncated sequences/timestamps.
var lastTime int64
if lsm != nil {
lastTime = lsm.ts
} else if smb != nil {
lastTime = smb.last.ts
} else {
lastTime = fs.state.LastTime.UnixNano()
if !hasLsm {
if smb != nil {
lastTime = smb.last.ts
} else {
lastTime = fs.state.LastTime.UnixNano()
}
}
fs.state.LastSeq = seq
fs.state.LastTime = time.Unix(0, lastTime).UTC()
@@ -10730,7 +10806,7 @@ func (fs *fileStore) Truncate(seq uint64) (rerr error) {
// If the selected block is not found or the message was deleted, we'll need to write a tombstone
// at the truncated sequence so we don't roll backward on our last sequence and timestamp.
if lsm == nil || removeSmb {
if !hasLsm || removeSmb {
if err = fs.writeTombstone(seq, lastTime); err != nil {
fs.mu.Unlock()
return err
@@ -11078,6 +11154,8 @@ func (mb *msgBlock) dirtyCloseWithRemove(remove bool) error {
}
}
if remove {
// The block is being destroyed, so mark it closed.
mb.closed = true
// Clear any tracking by subject if we are removing.
mb.fss = nil
if mb.mfn != _EMPTY_ {
@@ -11568,7 +11646,7 @@ func (fs *fileStore) flushStreamStateLoop(qch, done chan struct{}) {
fs.warn("File system permission denied when flushing stream state, disabling JetStream: %v", err)
// messages in block cache could be lost in the worst case.
// In the clustered mode it is very highly unlikely as a result of replication.
fs.srv.DisableJetStream()
fs.srv.ShutdownJetStream()
return
}
@@ -11652,13 +11730,12 @@ func (fs *fileStore) _writeFullState(force bool) error {
// We will base off of number of subjects and interior deletes. A very large number of msg blocks could also
// be used, but for next server version will redo all meta handling to be disk based. So this is temporary.
if !force {
const numThreshold = 1_000_000
// Calculate interior deletes.
var numDeleted int
if fs.state.LastSeq > fs.state.FirstSeq {
numDeleted = int((fs.state.LastSeq - fs.state.FirstSeq + 1) - fs.state.Msgs)
}
if numSubjects > numThreshold || numDeleted > numThreshold {
if numSubjects > highCardinalityThreshold || numDeleted > highCardinalityThreshold {
fs.mu.RUnlock()
return errStateTooBig
}
@@ -12832,16 +12909,31 @@ func (o *consumerFileStore) UpdateAcks(dseq, sseq uint64) error {
return ErrNoAckPolicy
}
var kick bool
defer func() {
if kick {
o.kickFlusher()
}
}()
// We do this regardless.
if _, ok := o.state.Redelivered[sseq]; ok {
delete(o.state.Redelivered, sseq)
kick = true
}
// On restarts the old leader may get a replay from the raft logs that are old.
if dseq <= o.state.AckFloor.Consumer {
return nil
}
if len(o.state.Pending) == 0 || o.state.Pending[sseq] == nil {
delete(o.state.Redelivered, sseq)
return ErrStoreMsgNotFound
}
// Done with the consistency checks, we'll always kick for below updates.
kick = true
// Check for AckAll here (or AckFlowControl which functions like AckAll).
if o.cfg.AckPolicy == AckAll || o.cfg.AckPolicy == AckFlowControl {
sgap := sseq - o.state.AckFloor.Stream
@@ -12860,7 +12952,6 @@ func (o *consumerFileStore) UpdateAcks(dseq, sseq uint64) error {
delete(o.state.Redelivered, seq)
}
}
o.kickFlusher()
return nil
}
@@ -12892,13 +12983,27 @@ func (o *consumerFileStore) UpdateAcks(dseq, sseq uint64) error {
}
}
}
// We do these regardless.
delete(o.state.Redelivered, sseq)
o.kickFlusher()
return nil
}
func (o *consumerFileStore) RemoveRedeliveredBelow(seq uint64) {
if seq == 0 {
return
}
o.mu.Lock()
defer o.mu.Unlock()
var removed bool
for s := range o.state.Redelivered {
if s < seq {
delete(o.state.Redelivered, s)
removed = true
}
}
if removed {
o.kickFlusher()
}
}
const seqsHdrSize = 6*binary.MaxVarintLen64 + hdrLen
// Encode our consumer state, version 2.
+70 -32
View File
@@ -209,9 +209,6 @@ func (s *Server) EnableJetStream(config *JetStreamConfig) error {
maxStore, maxMem = config.MaxStore, config.MaxMemory
}
config = s.dynJetStreamConfig(storeDir, maxStore, maxMem)
if maxMem > 0 {
config.MaxMemory = maxMem
}
if domain != _EMPTY_ {
config.Domain = domain
}
@@ -577,7 +574,7 @@ func (s *Server) restartJetStream() error {
err := s.EnableJetStream(&cfg)
if err != nil {
s.Warnf("Can't start JetStream: %v", err)
return s.DisableJetStream()
return s.ShutdownJetStream()
}
s.updateJetStreamInfoStatus(true)
return nil
@@ -629,7 +626,7 @@ func (s *Server) handleOutOfSpace(mset *stream) {
s.Errorf("JetStream out of resources, will be DISABLED")
}
go s.DisableJetStream()
go s.ShutdownJetStream()
adv := &JSServerOutOfSpaceAdvisory{
TypedEvent: TypedEvent{
@@ -648,8 +645,23 @@ func (s *Server) handleOutOfSpace(mset *stream) {
}
// DisableJetStream will turn off JetStream and signals in clustered mode
// to have the metacontroller remove us from the peer list.
// to have the metacontroller remove us from the peer list. Persistent
// meta-raft state on disk is removed. For transient runtime errors where
// the server should rejoin its existing meta group on restart, use
// ShutdownJetStream instead.
func (s *Server) DisableJetStream() error {
return s.disableJetStream(true)
}
// ShutdownJetStream is like DisableJetStream but preserves persistent
// meta-raft state on disk so the server can rejoin the existing meta
// group on restart. Use for transient runtime errors that the operator
// is expected to fix before restarting.
func (s *Server) ShutdownJetStream() error {
return s.disableJetStream(false)
}
func (s *Server) disableJetStream(deleteState bool) error {
if !s.JetStreamEnabled() {
return nil
}
@@ -680,7 +692,12 @@ func (s *Server) DisableJetStream() error {
s.Warnf("JetStream timeout waiting for meta leader transfer")
}
}
meta.Delete()
if deleteState {
meta.Delete()
} else {
meta.Stop()
meta.WaitForStop()
}
}
}
@@ -1811,13 +1828,15 @@ func diffCheckedLimits(a, b map[string]JetStreamAccountLimits) map[string]JetStr
// Lock should be held.
func (jsa *jsAccount) reservedStorage(tier string) (mem, store uint64) {
for _, mset := range jsa.streams {
cfg := &mset.cfg
if (tier == _EMPTY_ || tier == tierName(cfg.Replicas)) && cfg.MaxBytes > 0 {
switch cfg.Storage {
mset.cfgMu.RLock()
storage, replicas, maxBytes := mset.cfg.Storage, mset.cfg.Replicas, mset.cfg.MaxBytes
mset.cfgMu.RUnlock()
if (tier == _EMPTY_ || tier == tierName(replicas)) && maxBytes > 0 {
switch storage {
case FileStorage:
store += uint64(cfg.MaxBytes)
store += uint64(maxBytes)
case MemoryStorage:
mem += uint64(cfg.MaxBytes)
mem += uint64(maxBytes)
}
}
}
@@ -2332,9 +2351,9 @@ func tierName(replicas int) string {
return fmt.Sprintf("R%d", replicas)
}
func isSameTier(cfgA, cfgB *StreamConfig) bool {
a := max(1, cfgA.Replicas)
b := max(1, cfgB.Replicas)
func isSameTier(replicasA, replicasB int) bool {
a := max(1, replicasA)
b := max(1, replicasB)
// TODO (mh) this is where we could select based off a placement tag as well "qos:tier"
return a == b
}
@@ -2360,9 +2379,12 @@ func (jsa *jsAccount) selectLimits(replicas int) (JetStreamAccountLimits, string
// Lock should be held.
func (jsa *jsAccount) countStreams(tier string, cfg *StreamConfig) (streams int) {
for _, sa := range jsa.streams {
for _, mset := range jsa.streams {
mset.cfgMu.RLock()
name, replicas := mset.cfg.Name, mset.cfg.Replicas
mset.cfgMu.RUnlock()
// Don't count the stream toward the limit if it already exists.
if (tier == _EMPTY_ || isSameTier(&sa.cfg, cfg)) && sa.cfg.Name != cfg.Name {
if (tier == _EMPTY_ || isSameTier(replicas, cfg.Replicas)) && name != cfg.Name {
streams++
}
}
@@ -2426,53 +2448,69 @@ func (jsa *jsAccount) wouldExceedLimits(storeType StorageType, tierName string,
// Check account limits.
// Read Lock should be held
func (js *jetStream) checkAccountLimits(selected *JetStreamAccountLimits, config *StreamConfig, currentRes int64) error {
return js.checkLimits(selected, config, false, currentRes, 0)
func (js *jetStream) checkAccountLimits(selected *JetStreamAccountLimits, tier string, config *StreamConfig, currentRes int64) error {
return js.checkLimits(selected, tier, config, false, currentRes, 0)
}
// Check account and server limits.
// Read Lock should be held
func (js *jetStream) checkAllLimits(selected *JetStreamAccountLimits, config *StreamConfig, currentRes, maxBytesOffset int64) error {
return js.checkLimits(selected, config, true, currentRes, maxBytesOffset)
func (js *jetStream) checkAllLimits(selected *JetStreamAccountLimits, tier string, config *StreamConfig, currentRes, maxBytesOffset int64) error {
return js.checkLimits(selected, tier, config, true, currentRes, maxBytesOffset)
}
// Check if a new proposed msg set while exceed our account limits.
// Lock should be held.
func (js *jetStream) checkLimits(selected *JetStreamAccountLimits, config *StreamConfig, checkServer bool, currentRes, maxBytesOffset int64) error {
func (js *jetStream) checkLimits(selected *JetStreamAccountLimits, tier string, config *StreamConfig, checkServer bool, currentRes, maxBytesOffset int64) error {
// Check MaxConsumers
if config.MaxConsumers > 0 && selected.MaxConsumers > 0 && config.MaxConsumers > selected.MaxConsumers {
return NewJSMaximumConsumersLimitError()
}
// stream limit is checked separately on stream create only!
// Check storage, memory or disk.
return js.checkBytesLimits(selected, config.MaxBytes, config.Storage, checkServer, currentRes, maxBytesOffset)
return js.checkBytesLimits(selected, tier, config.MaxBytes, config.Replicas, config.Storage, checkServer, currentRes, maxBytesOffset)
}
// accountReservation returns how many bytes count against the account limit
// for a stream with the given replica count. Un-tiered limits are flat, so R>1
// is counted as Replicas*bytes; tiered limits already bake in replication.
func accountReservation(tier string, replicas int, bytes int64) int64 {
if bytes <= 0 {
return 0
}
if tier == _EMPTY_ && replicas > 1 {
return mulSaturate(int64(replicas), bytes)
}
return bytes
}
// Check if additional bytes will exceed our account limits and optionally the server itself.
// Read Lock should be held.
func (js *jetStream) checkBytesLimits(selectedLimits *JetStreamAccountLimits, addBytes int64, storage StorageType, checkServer bool, currentRes, maxBytesOffset int64) error {
func (js *jetStream) checkBytesLimits(selectedLimits *JetStreamAccountLimits, tier string, addBytes int64, replicas int, storage StorageType, checkServer bool, currentRes, maxBytesOffset int64) error {
if addBytes < 0 {
addBytes = 1
}
totalBytes := addSaturate(addBytes, maxBytesOffset)
// The per-server footprint is a single replica's worth of bytes; the
// account footprint additionally accounts for replication in un-tiered setups.
serverBytes := addSaturate(addBytes, maxBytesOffset)
accountBytes := accountReservation(tier, replicas, serverBytes)
switch storage {
case MemoryStorage:
// Account limits defined.
if selectedLimits.MaxMemory >= 0 && (currentRes > selectedLimits.MaxMemory || totalBytes > selectedLimits.MaxMemory-currentRes) {
if selectedLimits.MaxMemory >= 0 && (currentRes > selectedLimits.MaxMemory || accountBytes > selectedLimits.MaxMemory-currentRes) {
return NewJSMemoryResourcesExceededError()
}
// Check if this server can handle request.
if checkServer && (js.memReserved > js.config.MaxMemory || totalBytes > js.config.MaxMemory-js.memReserved) {
if checkServer && (js.memReserved > js.config.MaxMemory || serverBytes > js.config.MaxMemory-js.memReserved) {
return NewJSMemoryResourcesExceededError()
}
case FileStorage:
// Account limits defined.
if selectedLimits.MaxStore >= 0 && (currentRes > selectedLimits.MaxStore || totalBytes > selectedLimits.MaxStore-currentRes) {
if selectedLimits.MaxStore >= 0 && (currentRes > selectedLimits.MaxStore || accountBytes > selectedLimits.MaxStore-currentRes) {
return NewJSStorageResourcesExceededError()
}
// Check if this server can handle request.
if checkServer && (js.storeReserved > js.config.MaxStore || totalBytes > js.config.MaxStore-js.storeReserved) {
if checkServer && (js.storeReserved > js.config.MaxStore || serverBytes > js.config.MaxStore-js.storeReserved) {
return NewJSStorageResourcesExceededError()
}
}
@@ -2682,13 +2720,13 @@ func (s *Server) dynJetStreamConfig(storeDir string, maxStore, maxMem int64) *Je
jsc.SyncInterval = opts.SyncInterval
jsc.SyncAlways = opts.SyncAlways
if opts.maxStoreSet && maxStore >= 0 {
if maxStore > 0 || (opts.maxStoreSet && maxStore == 0) {
jsc.MaxStore = maxStore
} else {
jsc.MaxStore = diskAvailable(jsc.StoreDir)
}
if opts.maxMemSet && maxMem >= 0 {
if maxMem > 0 || (opts.maxMemSet && maxMem == 0) {
jsc.MaxMemory = maxMem
} else {
// Estimate to 75% of total memory if we can determine system memory.
@@ -2873,7 +2911,7 @@ func (s *Server) handleWritePermissionError() {
if s.JetStreamEnabled() {
s.Errorf("File system permission denied while writing, disabling JetStream")
go s.DisableJetStream()
go s.ShutdownJetStream()
//TODO Send respective advisory if needed, same as in handleOutOfSpace
}
+47 -27
View File
@@ -1352,19 +1352,16 @@ func (s *Server) jsonResponse(v any) string {
// Read lock must be held
func (jsa *jsAccount) tieredReservation(tier string, cfg *StreamConfig) int64 {
var reservation int64
for _, sa := range jsa.streams {
for _, mset := range jsa.streams {
mset.cfgMu.RLock()
name, storage, replicas, maxBytes := mset.cfg.Name, mset.cfg.Storage, mset.cfg.Replicas, mset.cfg.MaxBytes
mset.cfgMu.RUnlock()
// Don't count the stream toward the limit if it already exists.
if sa.cfg.Name == cfg.Name {
if name == cfg.Name {
continue
}
if (tier == _EMPTY_ || isSameTier(&sa.cfg, cfg)) && sa.cfg.MaxBytes > 0 && sa.cfg.Storage == cfg.Storage {
// If tier is empty, all storage is flat and we should adjust for replicas.
// Otherwise if tiered, storage replication already taken into consideration.
if tier == _EMPTY_ && sa.cfg.Replicas > 1 {
reservation = addSaturate(reservation, mulSaturate(int64(sa.cfg.Replicas), sa.cfg.MaxBytes))
} else {
reservation = addSaturate(reservation, sa.cfg.MaxBytes)
}
if (tier == _EMPTY_ || isSameTier(replicas, cfg.Replicas)) && maxBytes > 0 && storage == cfg.Storage {
reservation = addSaturate(reservation, accountReservation(tier, replicas, maxBytes))
}
}
return reservation
@@ -1699,19 +1696,23 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, _ *Account,
resp.Streams = resp.Streams[:JSApiNamesLimit]
}
} else {
// Snapshot names once to avoid repeated cfgMu RLocks during sort+append.
msets := acc.filteredStreams(filter)
// Since we page results order matters.
if len(msets) > 1 {
slices.SortFunc(msets, func(i, j *stream) int { return cmp.Compare(i.cfg.Name, j.cfg.Name) })
names := make([]string, len(msets))
for i, mset := range msets {
names[i] = mset.getCfgName()
}
if len(names) > 1 {
slices.Sort(names)
}
numStreams = len(msets)
numStreams = len(names)
if offset > numStreams {
offset = numStreams
}
for _, mset := range msets[offset:] {
resp.Streams = append(resp.Streams, mset.cfg.Name)
for _, name := range names[offset:] {
resp.Streams = append(resp.Streams, name)
if len(resp.Streams) >= JSApiNamesLimit {
break
}
@@ -1805,21 +1806,31 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, s
msets = acc.filteredStreams(filter)
}
slices.SortFunc(msets, func(i, j *stream) int { return cmp.Compare(i.cfg.Name, j.cfg.Name) })
// Snapshot names once and sort the parallel slice to avoid repeated cfgMu RLocks.
type msetWithName struct {
mset *stream
name string
}
named := make([]msetWithName, len(msets))
for i, mset := range msets {
named[i] = msetWithName{mset, mset.getCfgName()}
}
slices.SortFunc(named, func(a, b msetWithName) int { return cmp.Compare(a.name, b.name) })
scnt := len(msets)
scnt := len(named)
if offset > scnt {
offset = scnt
}
var missingNames []string
for _, mset := range msets[offset:] {
for _, n := range named[offset:] {
mset, name := n.mset, n.name
if mset.offlineReason != _EMPTY_ {
if resp.Offline == nil {
resp.Offline = make(map[string]string, 1)
}
resp.Offline[mset.getCfgName()] = mset.offlineReason
missingNames = append(missingNames, mset.getCfgName())
resp.Offline[name] = mset.offlineReason
missingNames = append(missingNames, name)
continue
}
@@ -3286,12 +3297,15 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, _ *Account, su
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
if mset.cfg.Sealed {
mset.cfgMu.RLock()
sealed, denyDelete := mset.cfg.Sealed, mset.cfg.DenyDelete
mset.cfgMu.RUnlock()
if sealed {
resp.Error = NewJSStreamSealedError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
if mset.cfg.DenyDelete {
if denyDelete {
resp.Error = NewJSStreamMsgDeleteFailedError(errors.New("message delete not permitted"))
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
@@ -3719,12 +3733,15 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, _ *Account,
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
if mset.cfg.Sealed {
mset.cfgMu.RLock()
sealed, denyPurge := mset.cfg.Sealed, mset.cfg.DenyPurge
mset.cfgMu.RUnlock()
if sealed {
resp.Error = NewJSStreamSealedError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
if mset.cfg.DenyPurge {
if denyPurge {
resp.Error = NewJSStreamPurgeFailedError(errors.New("stream purge not permitted"))
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
@@ -3762,7 +3779,7 @@ func (acc *Account) jsNonClusteredStreamLimitsCheck(cfg *StreamConfig) *ApiError
return NewJSMaximumStreamsLimitError()
}
reserved := jsa.tieredReservation(tier, cfg)
if err := jsa.js.checkAllLimits(selectedLimits, cfg, reserved, 0); err != nil {
if err := jsa.js.checkAllLimits(selectedLimits, tier, cfg, reserved, 0); err != nil {
return NewJSStreamLimitsError(err, Unless(err))
}
return nil
@@ -4297,6 +4314,8 @@ func (s *Server) streamSnapshot(acc *Account, mset *stream, sr *SnapshotResult,
var hdr []byte
chunk := make([]byte, chunkSize)
ackTimer := time.NewTimer(snapshotAckTimeout)
defer stopAndClearTimer(&ackTimer)
for index := 1; ; index++ {
select {
case <-slots:
@@ -4309,7 +4328,7 @@ func (s *Server) streamSnapshot(acc *Account, mset *stream, sr *SnapshotResult,
// The snapshotting goroutine has failed for some reason.
hdr = []byte(fmt.Sprintf("NATS/1.0 500 %s\r\n\r\n", err))
goto done
case <-time.After(snapshotAckTimeout):
case <-ackTimer.C:
// It's taking a very long time for the receiver to send us acks,
// they have probably stalled or there is high loss on the link.
hdr = []byte("NATS/1.0 408 No Flow Response\r\n\r\n")
@@ -4328,6 +4347,7 @@ func (s *Server) streamSnapshot(acc *Account, mset *stream, sr *SnapshotResult,
hdr = []byte("NATS/1.0 204\r\n\r\n")
}
mset.outq.send(newJSPubMsg(reply, _EMPTY_, ackReply, nil, chunk, nil, 0))
ackTimer.Reset(snapshotAckTimeout)
}
done:
+26 -2
View File
@@ -451,6 +451,7 @@ func (diff *batchStagedDiff) commit(mset *stream) {
if c, ok := mset.inflight[subj]; ok {
c.bytes += i.bytes
c.ops += i.ops
c.schedule = i.schedule
} else {
mset.inflight[subj] = i
}
@@ -530,13 +531,14 @@ func checkMsgHeadersPreClusteredProposal(
discard DiscardPolicy, discardNewPer bool, maxMsgSize int, maxMsgs int64, maxMsgsPer int64, maxBytes int64,
) ([]byte, []byte, uint64, *ApiError, error) {
var incr *big.Int
var hasSchedule bool
// Some header checks must be checked pre proposal.
if len(hdr) > 0 {
// Since we encode header len as u16 make sure we do not exceed.
// Again this works if it goes through but better to be pre-emptive.
if len(hdr) > math.MaxUint16 {
err := fmt.Errorf("JetStream header size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name)
err := fmt.Errorf("JetStream header size exceeds limits for '%s > %s'", jsa.acc().Name, name)
return hdr, msg, 0, NewJSStreamHeaderExceedsMaximumError(), err
}
// Counter increments.
@@ -810,6 +812,7 @@ func checkMsgHeadersPreClusteredProposal(
}
return hdr, msg, 0, apiErr, apiErr
} else if !schedule.IsZero() {
hasSchedule = true
if !allowMsgSchedules {
apiErr := NewJSMessageSchedulesDisabledError()
return hdr, msg, 0, apiErr, apiErr
@@ -877,6 +880,26 @@ func checkMsgHeadersPreClusteredProposal(
} else if !allowMsgSchedules {
apiErr := NewJSMessageSchedulesDisabledError()
return hdr, msg, 0, apiErr, apiErr
} else {
// Check that the to-be-purged subject is a schedule message.
// We still allow this message through if there exists no message for this subject,
// to remain backward-compatible. An "expected at sequence" check can still be
// performed to make this stricter.
schedSubj := bytesToString(scheduler)
var invalid bool
if i, ok := diff.inflight[schedSubj]; ok {
invalid = !i.schedule
} else if i, ok = mset.inflight[schedSubj]; ok {
invalid = !i.schedule
} else {
var smv StoreMsg
sm, _ := mset.store.LoadLastMsg(schedSubj, &smv)
invalid = sm != nil && len(sliceHeader(JSSchedulePattern, sm.hdr)) == 0
}
if invalid {
apiErr := NewJSMessageSchedulesSchedulerInvalidError()
return hdr, msg, 0, apiErr, apiErr
}
}
} else if !sourced && len(sliceHeader(JSScheduler, hdr)) > 0 {
// Clients may only use Nats-Scheduler alongside Nats-Schedule-Next.
@@ -930,8 +953,9 @@ func checkMsgHeadersPreClusteredProposal(
if i, ok = diff.inflight[subject]; ok {
i.bytes += sz
i.ops++
i.schedule = hasSchedule
} else {
i = &inflightSubjectRunningTotal{bytes: sz, ops: 1}
i = &inflightSubjectRunningTotal{bytes: sz, ops: 1, schedule: hasSchedule}
diff.inflight[subject] = i
}
+165 -97
View File
@@ -320,6 +320,21 @@ func (ca *consumerAssignment) clearResponded() {
ca.responded.Store(false)
}
// sameIdentity reports whether nca refers to the same logical consumer as ca.
// Only stable identity fields (Name, Stream, Group name, Created time) are
// compared; request-routing fields like Client/Reply and transient flags are
// intentionally excluded since processClusterCreateConsumer may set the
// per-object o.ca to a clone with the original requester's Client/Reply
// preserved while the meta-layer holds the newer values.
func (ca *consumerAssignment) sameIdentity(nca *consumerAssignment) bool {
return ca != nil && nca != nil &&
nca.Name == ca.Name &&
nca.Stream == ca.Stream &&
nca.Created.Equal(ca.Created) &&
nca.Group != nil && ca.Group != nil &&
nca.Group.Name == ca.Group.Name
}
// clone returns a copy of ca. Field-explicit (rather than `*ca`) and
// pointer-returning so the embedded atomic.Bool isn't value-copied;
// responded is transferred via Load/Store. Concurrent callers may write
@@ -713,6 +728,14 @@ func (js *jetStream) isStreamHealthy(acc *Account, sa *streamAssignment) error {
js.mu.RUnlock()
return errors.New("stream assignment or group missing")
}
// Surface any persisted assignment-level error (e.g. failed create on this
// peer due to account limits) so the health check reflects the broken state
// instead of falling through to runtime-only checks.
if sa.err != nil {
err := sa.err
js.mu.RUnlock()
return fmt.Errorf("stream assignment error: %w", err)
}
streamName := sa.Config.Name
node := sa.Group.node
js.mu.RUnlock()
@@ -788,6 +811,14 @@ func (js *jetStream) isConsumerHealthy(mset *stream, consumer string, ca *consum
js.mu.RUnlock()
return errors.New("consumer assignment or group missing")
}
// Surface any persisted assignment-level error (e.g. failed create on this
// peer) so the health check reflects the broken state instead of falling
// through to runtime-only checks.
if ca.err != nil {
err := ca.err
js.mu.RUnlock()
return fmt.Errorf("consumer assignment error: %w", err)
}
created := ca.Created
node := ca.Group.node
js.mu.RUnlock()
@@ -3612,7 +3643,7 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps
// If we were successful lookup up our stream now.
if err == nil {
if mset, err = acc.lookupStream(sa.Config.Name); mset != nil {
mset.monitorWg.Add(1)
mset.startMonitorWg()
defer mset.monitorWg.Done()
mset.checkInMonitor()
mset.setStreamAssignment(sa)
@@ -3632,6 +3663,7 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps
mset.delete()
}
js.mu.Lock()
s.Warnf("Stream restore failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err)
sa.err = err
if n != nil {
n.Delete()
@@ -3792,8 +3824,7 @@ func (mset *stream) resetClusteredState(err error) bool {
// Need to do the rest in a separate Go routine.
go func() {
mset.signalMonitorQuit()
mset.monitorWg.Wait()
mset.stopMonitoring()
mset.resetAndWaitOnConsumers()
// Stop our stream.
mset.stop(shouldDelete, false)
@@ -4526,6 +4557,10 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) {
return
}
// Acquire clMu before ddMu so any inflight proposals finish first, and we can
// clean up if they added new dedupe IDs.
mset.clMu.Lock()
// Clear inflight dedupe IDs, where seq=0.
mset.ddMu.Lock()
var removed int
@@ -4548,7 +4583,6 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) {
}
mset.ddMu.Unlock()
mset.clMu.Lock()
// Clear inflight if we have it.
mset.inflight = nil
mset.inflightTransform = nil
@@ -4557,6 +4591,12 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) {
// Clear expected per subject state.
mset.expectedPerSubjectSequence = nil
mset.expectedPerSubjectInProcess = nil
// Clear clseq on every leader transition. recalculateClusteredSeq
// repopulates it on the next proposal.
if mset.clseq > 0 {
mset.clseq = 0
}
mset.clMu.Unlock()
js.mu.RLock()
@@ -4578,14 +4618,6 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) {
}
}
// Clear clseq on every leader transition. recalculateClusteredSeq
// repopulates it on the next proposal.
mset.clMu.Lock()
if mset.clseq > 0 {
mset.clseq = 0
}
mset.clMu.Unlock()
// Tell stream to switch leader status.
mset.setLeader(isLeader)
@@ -5041,14 +5073,14 @@ func (s *Server) removeStream(mset *stream, nsa *streamAssignment) {
if js, _ := s.getJetStreamCluster(); js != nil {
js.mu.Lock()
nsa.Group.node = nil
nsa.err = nil
isShuttingDown = js.shuttingDown
js.mu.Unlock()
}
if !isShuttingDown {
// wait for monitor to be shutdown.
mset.signalMonitorQuit()
mset.monitorWg.Wait()
mset.stopMonitoring()
}
mset.stop(true, false)
}
@@ -5068,6 +5100,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
storage, cfg := sa.Config.Storage, sa.Config
recovering := sa.recovering
hasResponded := sa.markResponded()
hadErr := sa.err != nil
js.mu.RUnlock()
mset, err := acc.lookupStream(cfg.Name)
@@ -5077,8 +5110,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
s.Warnf("JetStream cluster detected stream remapping for '%s > %s' from %q to %q",
acc, cfg.Name, osa.Group.Name, sa.Group.Name)
mset.removeNode()
mset.signalMonitorQuit()
mset.monitorWg.Wait()
mset.stopMonitoring()
alreadyRunning, needsNode = false, true
// Make sure to clear from original.
js.mu.Lock()
@@ -5103,7 +5135,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
"stream": mset.name(),
})
}
mset.monitorWg.Add(1)
mset.startMonitorWg()
// Start monitoring..
started := s.startGoRoutine(
func() { js.monitorStream(mset, sa, needsNode) },
@@ -5119,8 +5151,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
} else if numReplicas == 1 && alreadyRunning {
// We downgraded to R1. Make sure we cleanup the raft node and the stream monitor.
mset.removeNode()
mset.signalMonitorQuit()
mset.monitorWg.Wait()
mset.stopMonitoring()
// In case we need to shutdown the cluster specific subs, etc.
mset.mu.Lock()
// Stop responding to sync requests.
@@ -5137,9 +5168,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
mset.setStreamAssignment(sa)
// Call update.
if err = mset.updateWithAdvisory(cfg, !recovering, false); err != nil {
s.Warnf("JetStream cluster error updating stream %q for account %q: %v", cfg.Name, acc.Name, err)
}
err = mset.updateWithAdvisory(cfg, !recovering, false)
}
// If not found we must be expanding into this node since if we are here we know we are a member.
@@ -5150,6 +5179,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
if err != nil {
js.mu.Lock()
s.Warnf("Stream update failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err)
sa.err = err
result := &streamAssignmentResult{
Account: sa.Client.serviceAccount(),
@@ -5163,6 +5193,10 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
// Send response to the metadata leader. They will forward to the user as needed.
s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result)
return
} else if hadErr {
js.mu.Lock()
sa.err = nil
js.mu.Unlock()
}
isLeader := mset.IsLeader()
@@ -5219,6 +5253,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
storage := sa.Config.Storage
restore := sa.Restore
recovering := sa.recovering
hadErr := sa.err != nil
js.mu.RUnlock()
// Process the raft group and make sure it's running if needed.
@@ -5310,7 +5345,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
}
} else if err == NewJSStreamNotFoundError() {
// Add in the stream here.
mset, err = acc.addStreamWithAssignment(sa.Config, nil, sa, false, false)
mset, err = acc.addStreamWithAssignment(sa.Config, nil, sa, false, true)
}
if mset != nil {
mset.setCreatedTime(created)
@@ -5327,8 +5362,8 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
return
}
s.Warnf("Stream create failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err)
if IsNatsErr(err, JSStreamStoreFailedF) {
s.Warnf("Stream create failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err)
err = errStreamStoreFailed
}
js.mu.Lock()
@@ -5361,6 +5396,10 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result)
}
return
} else if hadErr {
js.mu.Lock()
sa.err = nil
js.mu.Unlock()
}
// Re-capture node.
@@ -5372,7 +5411,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
if node != nil {
if !alreadyRunning {
if mset != nil {
mset.monitorWg.Add(1)
mset.startMonitorWg()
}
started := s.startGoRoutine(
func() { js.monitorStream(mset, sa, false) },
@@ -5408,6 +5447,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
mset.delete()
}
js.mu.Lock()
s.Warnf("Stream restore failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err)
sa.err = err
result := &streamAssignmentResult{
Account: sa.Client.serviceAccount(),
@@ -5559,8 +5599,7 @@ func (js *jetStream) processClusterDeleteStream(sa *streamAssignment, isMember,
n.Delete()
}
// wait for monitor to be shut down
mset.signalMonitorQuit()
mset.monitorWg.Wait()
mset.stopMonitoring()
err = mset.stop(true, wasLeader)
stopped = true
} else if isMember {
@@ -5782,8 +5821,7 @@ func (s *Server) removeConsumer(o *consumer, nca *consumerAssignment) {
if !isShuttingDown {
// wait for monitor to be shutdown.
o.signalMonitorQuit()
o.monitorWg.Wait()
o.stopMonitoring()
}
o.deleteWithoutAdvisory()
}
@@ -5890,8 +5928,7 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
s.Warnf("JetStream cluster detected consumer remapping for '%s > %s' from %q to %q",
acc, ca.Name, oca.Group.Name, ca.Group.Name)
o.clearNode()
o.signalMonitorQuit()
o.monitorWg.Wait()
o.stopMonitoring()
alreadyRunning = false
// Make sure to clear from original.
js.mu.Lock()
@@ -5999,13 +6036,12 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
return
}
s.Warnf("Consumer create failed for '%s > %s > %s': %v", ca.Client.serviceAccount(), ca.Stream, ca.Name, err)
if IsNatsErr(err, JSConsumerStoreFailedErrF) {
s.Warnf("Consumer create failed for '%s > %s > %s': %v", ca.Client.serviceAccount(), ca.Stream, ca.Name, err)
err = errConsumerStoreFailed
}
js.mu.Lock()
ca.err = err
hasResponded := ca.hasResponded()
@@ -6048,8 +6084,14 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
}
} else {
js.mu.RLock()
hadErr := ca.err != nil
node := rg.node
js.mu.RUnlock()
if hadErr {
js.mu.Lock()
ca.err = nil
js.mu.Unlock()
}
if didCreate {
o.setCreatedTime(ca.Created)
@@ -6057,8 +6099,7 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
// Check for scale down to 1..
if node != nil && len(rg.Peers) == 1 {
o.clearNode()
o.signalMonitorQuit()
o.monitorWg.Wait()
o.stopMonitoring()
// Need to clear from rg too.
js.mu.Lock()
rg.node = nil
@@ -6097,8 +6138,7 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
if node == nil {
// Wait for the previous routine to stop running.
o.signalMonitorQuit()
o.monitorWg.Wait()
o.stopMonitoring()
// Single replica consumer, process manually here.
// Force response in case we think this is an update.
if !js.isMetaRecovering() && isConfigUpdate {
@@ -6129,8 +6169,7 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
// Start our monitoring routine if needed.
if !alreadyRunning {
// Wait for the previous routine to stop running.
o.signalMonitorQuit()
o.monitorWg.Wait()
o.stopMonitoring()
if o.shouldStartMonitor() {
started := s.startGoRoutine(
func() { js.monitorConsumer(o, ca) },
@@ -7199,9 +7238,17 @@ func (js *jetStream) processStreamAssignmentResults(sub *subscription, c *client
}
// Remove this assignment if possible.
if canDelete {
var apiErr *ApiError
if result.Response != nil {
apiErr = result.Response.Error
} else if result.Restore != nil {
apiErr = result.Restore.Error
}
s.Warnf("Stream assignment for '%s > %s' rejected by assigned member: %v", sa.Client.serviceAccount(), sa.Config.Name, apiErr)
sa.err = NewJSClusterNotAssignedError()
cc.meta.Propose(encodeDeleteStreamAssignment(sa))
cc.trackInflightStreamProposal(result.Account, sa, true)
if err := cc.meta.Propose(encodeDeleteStreamAssignment(sa)); err == nil {
cc.trackInflightStreamProposal(result.Account, sa, true)
}
}
}
}
@@ -7236,6 +7283,7 @@ func (js *jetStream) processConsumerAssignmentResults(sub *subscription, c *clie
// Make sure this is recent response.
if result.Response.Error != nil && result.Response.Error != NewJSConsumerNameExistError() && time.Since(ca.Created) < 2*time.Second {
// Do not list in consumer names/lists.
s.Warnf("Consumer assignment for '%s > %s > %s' rejected by assigned member: %v", ca.Client.serviceAccount(), ca.Stream, ca.Name, result.Response.Error)
ca.err = NewJSClusterNotAssignedError()
}
}
@@ -7866,16 +7914,10 @@ func (js *jetStream) tieredStreamAndReservationCount(accName, tier string, cfg *
if sa.Config.Name == cfg.Name {
continue
}
if tier == _EMPTY_ || isSameTier(sa.Config, cfg) {
if tier == _EMPTY_ || isSameTier(sa.Config.Replicas, cfg.Replicas) {
numStreams++
if sa.Config.MaxBytes > 0 && sa.Config.Storage == cfg.Storage {
// If tier is empty, all storage is flat and we should adjust for replicas.
// Otherwise if tiered, storage replication already taken into consideration.
if tier == _EMPTY_ && sa.Config.Replicas > 1 {
reservation = addSaturate(reservation, mulSaturate(int64(sa.Config.Replicas), sa.Config.MaxBytes))
} else {
reservation = addSaturate(reservation, sa.Config.MaxBytes)
}
reservation = addSaturate(reservation, accountReservation(tier, sa.Config.Replicas, sa.Config.MaxBytes))
}
}
}
@@ -7951,7 +7993,7 @@ func (js *jetStream) jsClusteredStreamLimitsCheck(acc *Account, cfg *StreamConfi
return NewJSMaximumStreamsLimitError()
}
// Check for account limits here before proposing.
if err := js.checkAccountLimits(selectedLimits, cfg, reservations); err != nil {
if err := js.checkAccountLimits(selectedLimits, tier, cfg, reservations); err != nil {
return NewJSStreamLimitsError(err, Unless(err))
}
return nil
@@ -8298,26 +8340,7 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su
js.mu.Lock()
}
// If we identified a leader make sure its part of the new group.
selected := make([]string, 0, newCfg.Replicas)
if curLeader != _EMPTY_ {
selected = append(selected, curLeader)
}
for _, peer := range rg.Peers {
if len(selected) == newCfg.Replicas {
break
}
if peer == curLeader {
continue
}
if si, ok := s.nodeToInfo.Load(peer); ok && si != nil {
if si.(nodeInfo).offline {
continue
}
selected = append(selected, peer)
}
}
rg.Peers = selected
rg.Peers = s.selectScaleDownPeers(rg.Peers, curLeader, newCfg.Replicas)
// Single nodes are not recorded by the NRG layer so we can rename.
// MUST do this, otherwise a scaleup afterward could potentially lead to inconsistencies.
if len(rg.Peers) == 1 {
@@ -8639,6 +8662,43 @@ func (s *Server) allPeersOffline(rg *raftGroup) bool {
return true
}
// Select the peers to keep when scaling a raft group down to replicas.
// The current leader, if known and in peer set, is kept. Online peers are preferred,
// but we will fall back to offline peers to honor the requested replica count.
func (s *Server) selectScaleDownPeers(peers []string, curLeader string, replicas int) []string {
selected := make([]string, 0, replicas)
if curLeader != _EMPTY_ && slices.Contains(peers, curLeader) {
selected = append(selected, curLeader)
if len(selected) == replicas {
return selected
}
}
// Prefer online peers.
for _, peer := range peers {
if peer == curLeader {
continue
}
if si, ok := s.nodeToInfo.Load(peer); ok && si != nil && !si.(nodeInfo).offline {
selected = append(selected, peer)
if len(selected) == replicas {
return selected
}
}
}
// Fall back to offline peers for the remainder.
for _, peer := range peers {
if slices.Contains(selected, peer) {
continue
}
selected = append(selected, peer)
if len(selected) == replicas {
break
}
}
return selected
}
// This will do a scatter and gather operation for all streams for this account. This is only called from metadata leader.
// This will be running in a separate Go routine.
func (s *Server) jsClusteredStreamListRequest(acc *Account, ci *ClientInfo, filter string, offset int, subject, reply string, rmsg []byte) {
@@ -9423,6 +9483,11 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
return
}
if cfg.DeliverPolicy != DeliverAll {
resp.Error = NewJSConsumerWQConsumerNotDeliverAllError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
return
}
subjects := gatherSubjectFilters(cfg.FilterSubject, cfg.FilterSubjects)
for oca := range js.consumerAssignmentsOrInflightSeq(acc.Name, stream) {
if oca.Name == oname || oca.Config.Direct || oca.Config.Sourcing {
@@ -9479,7 +9544,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
if ci, err := sysRequest[ConsumerInfo](s, clusterConsumerInfoT, ci.serviceAccount(), sa.Config.Name, oname); err != nil {
s.Warnf("Did not receive consumer info results for '%s > %s > %s' due to: %s", acc, sa.Config.Name, oname, err)
} else if ci != nil {
if cl := ci.Cluster; cl != nil {
if cl := ci.Cluster; cl != nil && cl.Leader != _EMPTY_ {
curLeader = getHash(cl.Leader)
}
}
@@ -9523,20 +9588,9 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
nca.Group.Preferred = curLeader
nca.Group.ScaleUp = true
} else if rBefore > rAfter {
newPeerSet := nca.Group.Peers
// mark leader preferred and move it to end
// Mark the current leader as preferred, it will be kept in the new peer set.
nca.Group.Preferred = curLeader
if nca.Group.Preferred != _EMPTY_ {
for i, p := range newPeerSet {
if nca.Group.Preferred == p {
newPeerSet[i] = newPeerSet[len(newPeerSet)-1]
newPeerSet[len(newPeerSet)-1] = p
}
}
}
// scale down by removing peers from the end
newPeerSet = newPeerSet[len(newPeerSet)-rAfter:]
nca.Group.Peers = newPeerSet
nca.Group.Peers = s.selectScaleDownPeers(nca.Group.Peers, curLeader, rAfter)
// Single nodes are not recorded by the NRG layer so we can rename.
// MUST do this, otherwise a scaleup afterward could potentially lead to inconsistencies.
if len(nca.Group.Peers) == 1 {
@@ -9978,7 +10032,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [
// Check msgSize if we have a limit set there. Again this works if it goes through but better to be pre-emptive.
// Subtract to prevent against overflows.
if maxMsgSize >= 0 && (len(hdr) > maxMsgSize || len(msg) > maxMsgSize-len(hdr)) {
err := fmt.Errorf("JetStream message size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name)
err := fmt.Errorf("JetStream message size exceeds limits for '%s > %s'", jsa.acc().Name, name)
s.RateLimitWarnf("%s", err.Error())
if canRespond {
var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}}
@@ -10202,6 +10256,17 @@ var (
errCatchupTooManyRetries = errors.New("catchup failed, too many retries")
)
// Catchup inactivity timers.
const (
defaultStreamCatchupStartInterval = 5 * time.Second
defaultStreamCatchupActivityInterval = 30 * time.Second
)
var (
streamCatchupStartInterval = defaultStreamCatchupStartInterval
streamCatchupActivityInterval = defaultStreamCatchupActivityInterval
)
// Process a stream snapshot.
func (mset *stream) processSnapshot(snap *StreamReplicatedState, index uint64) (e error) {
// Update any deletes, etc.
@@ -10260,10 +10325,8 @@ func (mset *stream) processSnapshot(snap *StreamReplicatedState, index uint64) (
var sub *subscription
var err error
const (
startInterval = 5 * time.Second
activityInterval = 30 * time.Second
)
startInterval := streamCatchupStartInterval
activityInterval := streamCatchupActivityInterval
notActive := time.NewTimer(startInterval)
defer notActive.Stop()
@@ -10275,9 +10338,7 @@ func (mset *stream) processSnapshot(snap *StreamReplicatedState, index uint64) (
mset.mu.Lock()
for _, o := range mset.consumers {
o.mu.Lock()
if o.isLeader() {
o.streamNumPending()
}
o.streamNumPending()
o.mu.Unlock()
}
mset.mu.Unlock()
@@ -10462,8 +10523,11 @@ RETRY:
return err
} else if err == NewJSInsufficientResourcesError() {
notifyLeaderStopCatchup(mrec, err)
if mset.js.limitsExceeded(mset.cfg.Storage) {
s.resourcesExceededError(mset.cfg.Storage)
mset.cfgMu.RLock()
storage := mset.cfg.Storage
mset.cfgMu.RUnlock()
if mset.js.limitsExceeded(storage) {
s.resourcesExceededError(storage)
} else {
s.Warnf("Catchup for stream '%s > %s' errored, account resources exceeded: %v", mset.account(), mset.name(), err)
}
@@ -10914,7 +10978,7 @@ func (mset *stream) runCatchup(sendSubject string, sreq *streamSyncRequest) {
nextBatchC <- struct{}{}
remoteQuitCh := make(chan struct{})
const activityInterval = 30 * time.Second
activityInterval := streamCatchupActivityInterval
notActive := time.NewTimer(activityInterval)
defer notActive.Stop()
@@ -11167,6 +11231,8 @@ func (mset *stream) runCatchup(sendSubject string, sreq *streamSyncRequest) {
// Run as long as we are still active and need catchup.
// FIXME(dlc) - Purge event? Stream delete?
retryTimer := time.NewTimer(500 * time.Millisecond)
defer stopAndClearTimer(&retryTimer)
for {
// Get this each time, will be non-nil if globally blocked and we will close to wake everyone up.
cbKick := s.cbKickChan()
@@ -11181,7 +11247,8 @@ func (mset *stream) runCatchup(sendSubject string, sreq *streamSyncRequest) {
return
case <-notActive.C:
s.Warnf("Catchup for stream '%s > %s' stalled", mset.account(), mset.name())
mset.clearCatchupPeer(sreq.Peer)
// Do NOT clear the catchup peer on a transient inactivity stall, this allows the
// follower to retry without us losing track of it requiring catchup.
return
case <-nextBatchC:
if !sendNextBatchAndContinue(qch) {
@@ -11193,12 +11260,13 @@ func (mset *stream) runCatchup(sendSubject string, sreq *streamSyncRequest) {
mset.clearCatchupPeer(sreq.Peer)
return
}
case <-time.After(500 * time.Millisecond):
case <-retryTimer.C:
if !sendNextBatchAndContinue(qch) {
mset.clearCatchupPeer(sreq.Peer)
return
}
}
retryTimer.Reset(500 * time.Millisecond)
}
}
+72 -21
View File
@@ -304,6 +304,13 @@ func validateLeafNode(o *Options) error {
return fmt.Errorf("remote leaf node configuration cannot have a mix of websocket and non-websocket urls: %q", redactURLList(rcfg.URLs))
}
}
if !wsAllowedFIPS() {
for _, u := range rcfg.URLs {
if isWSURL(u) {
return fmt.Errorf("remote leaf node URL %q cannot be used in FIPS-140 mode when built with this Go version, use Go 1.26 or later", redactURLString(u.String()))
}
}
}
// Validate compression settings
if rcfg.Compression.Mode != _EMPTY_ {
if err := validateAndNormalizeCompressionOption(&rcfg.Compression, CompressionS2Auto); err != nil {
@@ -1300,7 +1307,10 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
info = s.copyLeafNodeInfo()
// For tests that want to simulate old servers, do not set the compression
// on the INFO protocol if configured with CompressionNotSupported.
if cm := opts.LeafNode.Compression.Mode; cm != CompressionNotSupported {
// Also suppress it if WebSocket compression is already in use, otherwise
// an old soliciting peer would honor the advertised mode, switch to S2,
// and then wait forever for a compressed INFO response from us.
if cm := opts.LeafNode.Compression.Mode; cm != CompressionNotSupported && (ws == nil || !ws.compress) {
info.Compression = cm
}
// We always send a nonce for LEAF connections. Do not change that without
@@ -1721,6 +1731,15 @@ func (c *client) processLeafnodeInfo(info *Info) {
}
func (s *Server) negotiateLeafCompression(c *client, didSolicit bool, infoCompression string, co *CompressionOpts) (bool, error) {
// If WebSocket compression is already negotiated on this connection then
// we shouldn't layer S2 compression on top of it.
c.mu.Lock()
if c.ws != nil && c.ws.compress {
c.leaf.compression = CompressionOff
c.mu.Unlock()
return false, nil
}
c.mu.Unlock()
// Negotiate the appropriate compression mode (or no compression)
cm, err := selectCompressionMode(co.Mode, infoCompression)
if err != nil {
@@ -2027,12 +2046,14 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
// In an extension use case, pin leadership to server remotes connect to.
// Therefore, server with a remote that are not already in observer mode, need to be put into it.
if solicited && meta != nil && !meta.IsObserver() {
meta.setObserver(true, extExtended)
c.Debugf("Turning JetStream metadata controller Observer Mode on - System Account Connected")
// Take note that the domain was not extended to avoid this state next startup.
writePeerState(js.config.StoreDir, meta.currentPeerState())
// If this server is the leader already, step down so a new leader can be elected (that is not an observer)
meta.StepDown()
// Discard any local metagroup state accumulated before the SYS-account
// leaf came up (e.g. the wrong-hint case where this server bootstrapped
// its own metagroup). The parent's view is now authoritative; without
// this reset the two raft logs stay forked because the standalone log's
// commit prefix short-circuits the follower's AE handling.
meta.setObserver(true, extExtended)
meta.Reset()
}
} else {
// This deny is needed in all cases (system account shared or not)
@@ -2586,31 +2607,48 @@ func (acc *Account) updateLeafNodesEx(sub *subscription, delta int32, hubOnly bo
// Do this once.
subject := string(sub.subject)
// Walk the connected leafnodes.
for _, ln := range acc.lleafs {
// Walk the connected leafnodes from a random starting point to avoid
// concurrent callers all contending over leafs in the same order.
nleafs := len(acc.lleafs)
start := 0
if nleafs > 1 {
start = rand.Intn(nleafs)
}
for i := 0; i < nleafs; i++ {
ln := acc.lleafs[(start+i)%nleafs]
if ln == sub.client {
continue
}
ln.mu.Lock()
ln.mu.RLock()
// Don't advertise interest from leafnodes to other isolated leafnodes.
if sub.client.kind == LEAF && ln.isIsolatedLeafNode() {
ln.mu.Unlock()
ln.mu.RUnlock()
continue
}
// If `hubOnly` is true, it means that we want to update only leafnodes
// that connect to this server (so isHubLeafNode() would return `true`).
if hubOnly && !ln.isHubLeafNode() {
ln.mu.Unlock()
ln.mu.RUnlock()
continue
}
// Check to make sure this sub does not have an origin cluster that matches the leafnode.
// If skipped, make sure that we still let go the "$LDS." subscription that allows
// the detection of loops as long as different cluster.
clusterDifferent := cluster != ln.remoteCluster()
if (isLDS && clusterDifferent) || ((cluster == _EMPTY_ || clusterDifferent) && (delta <= 0 || ln.canSubscribe(subject))) {
ln.updateSmap(sub, delta, isLDS)
update := (isLDS && clusterDifferent) ||
((cluster == _EMPTY_ || clusterDifferent) && (delta <= 0 || ln.canSubscribeInternal(subject)))
ln.mu.RUnlock()
if update {
ln.mu.Lock()
// The leaf role, isolation mode, and remote cluster are stable
// for the connection. Recheck canSubscribe here since permissions
// can change, and to initializes mperms for wildcard subscriptions
// that collide with deny rules.
if isLDS || delta <= 0 || ln.canSubscribe(subject) {
ln.updateSmap(sub, delta, isLDS)
}
ln.mu.Unlock()
}
ln.mu.Unlock()
}
}
@@ -3299,35 +3337,48 @@ func (c *client) leafMsgAllowed() bool {
return true
}
c.mu.Lock()
defer c.mu.Unlock()
c.mu.RLock()
if c.isSpokeLeafNode() {
// Gateway routed replies are forwarded without
// permission checks.
if isGW || c.leafReceiveAllowed(subjectToCheck) {
c.mu.RUnlock()
return true
}
} else if c.leafSendAllowed(subjectToCheck) {
c.mu.RUnlock()
return true
}
// If allow_responses is not configured, or there is no tracked reply for
// this subject, the answer is "denied" and we can return it while still
// holding only the read lock.
replySubject := bytesToString(wireSubject)
if c.perms == nil || c.perms.resp == nil || c.replies[replySubject] == nil {
c.mu.RUnlock()
return false
}
c.mu.RUnlock()
// Check tracked reply permissions (allow_responses).
// Use the pre-strip subject since deliverMsg tracks
// replies under the original form, which includes
// the GW routing prefix for routed requests.
return c.responseAllowed(bytesToString(wireSubject))
c.mu.Lock()
defer c.mu.Unlock()
return c.responseAllowed(replySubject)
}
// Returns true if the leaf side ACLs allow importing this subject,
// based on the permissions received over INFO and any local deny_imports.
// Lock must be held.
// At least a read lock must be held.
func (c *client) leafReceiveAllowed(subject []byte) bool {
return c.canSubscribe(bytesToString(subject))
return c.canSubscribeInternal(bytesToString(subject))
}
// Returns true if the hub side ACLs allow the remote leaf to send
// this subject.
// Lock must be held.
// At least a read lock must be held.
func (c *client) leafSendAllowed(bsubject []byte) bool {
// Use the original export ACL captured for this accepted leaf.
// The live perms also contain additional JetStream denies used by
+95 -51
View File
@@ -123,7 +123,7 @@ func (ms *memStore) UpdateConfig(cfg *StreamConfig) error {
maxp := ms.maxp
ms.maxp = cfg.MaxMsgsPer
// If the value is smaller, or was unset before, we need to enforce that.
if ms.maxp > 0 && (maxp == 0 || ms.maxp < maxp) {
if ms.maxp > 0 && (maxp <= 0 || ms.maxp < maxp) {
lm := uint64(ms.maxp)
ms.fss.IterFast(func(subj []byte, ss *SimpleState) bool {
if ss.Msgs > lm {
@@ -961,7 +961,7 @@ func (ms *memStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerS
var havePartial bool
var totalSkipped uint64
// We will track start and end sequences as we go.
stree.IntersectGSL[SimpleState](ms.fss, sl, func(subj []byte, fss *SimpleState) {
stree.IntersectGSL[SimpleState](ms.fss, sl, func(subj []byte, fss *SimpleState) bool {
if fss.firstNeedsUpdate || fss.lastNeedsUpdate {
ms.recalculateForSubj(bytesToString(subj), fss)
}
@@ -975,6 +975,7 @@ func (ms *memStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerS
} else {
totalSkipped += fss.Msgs
}
return true
})
// If we did not encounter any partials we can return here.
@@ -1426,48 +1427,70 @@ func (ms *memStore) runMsgScheduling() {
// PurgeEx will remove messages based on subject filters, sequence and number of messages to keep.
// Will return the number of purged messages.
func (ms *memStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) {
// sequence == 1 means "purge up to but not including 1", a no-op.
if sequence == 1 {
return 0, nil
}
if subject == _EMPTY_ || subject == fwcs {
if keep == 0 && sequence == 0 {
return ms.purge(0)
}
if sequence > 1 {
return ms.compact(sequence)
} else if keep > 0 {
ms.mu.RLock()
msgs, lseq := ms.state.Msgs, ms.state.LastSeq
ms.mu.RUnlock()
if keep >= msgs {
return 0, nil
}
return ms.compact(lseq - keep + 1)
}
return 0, nil
// Make sure to not leave subject if empty.
if subject == _EMPTY_ {
subject = fwcs
}
}
eq := compareFn(subject)
if ss, _ := ms.FilteredState(1, subject); ss.Msgs > 0 {
if keep > 0 {
if keep >= ss.Msgs {
return 0, nil
}
ss.Msgs -= keep
// FilteredState narrows the search range.
ss, _ := ms.FilteredState(1, subject)
if ss.Msgs == 0 {
return 0, nil
}
// If we have a "keep" designation need to know how many to purge.
var maxp uint64
if keep > 0 {
if keep >= ss.Msgs {
return 0, nil
}
last := ss.Last
if sequence > 1 {
last = sequence - 1
}
ms.mu.Lock()
for seq := ss.First; seq <= last; seq++ {
if sm, ok := ms.msgs[seq]; ok && eq(sm.subj, subject) {
if ok := ms.removeMsg(sm.seq, false); ok {
purged++
if purged >= ss.Msgs {
break
}
maxp = ss.Msgs - keep
}
// "Purge up to but not including sequence": sequence == 0 means no
// sequence filter; sequence >= 1 clamps the upper bound to sequence-1
// (so sequence == 1 purges nothing).
last := ss.Last
if sequence >= 1 {
last = sequence - 1
}
var bytes, lowSeq uint64
var lowSubj string
ms.mu.Lock()
for seq := ss.First; seq <= last; seq++ {
if sm, ok := ms.msgs[seq]; ok && eq(sm.subj, subject) {
if subj, sz, ok := ms.removeMsgNoCB(sm.seq, false); ok {
purged++
bytes += sz
if lowSeq == 0 {
lowSeq, lowSubj = sm.seq, subj
}
if maxp > 0 && purged >= maxp {
break
}
}
}
ms.mu.Unlock()
}
cb := ms.scb
ms.mu.Unlock()
if cb != nil && purged > 0 {
if purged == 1 {
cb(-1, -int64(bytes), lowSeq, lowSubj)
} else {
cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
}
}
return purged, nil
}
@@ -2124,20 +2147,38 @@ func (ms *memStore) recalculateForSubj(subj string, ss *SimpleState) {
// Removes the message referenced by seq.
// Lock should be held.
func (ms *memStore) removeMsg(seq uint64, secure bool) bool {
var ss uint64
sm, ok := ms.msgs[seq]
subj, size, ok := ms.removeMsgNoCB(seq, secure)
if !ok {
return false
}
if ms.scb != nil {
// We do not want to hold any locks here.
ms.mu.Unlock()
if ms.scb != nil {
ms.scb(-1, -int64(size), seq, subj)
}
ms.mu.Lock()
}
return true
}
ss = memStoreMsgSize(sm.subj, sm.hdr, sm.msg)
// Removes the message referenced by seq, but without calling the storage callback.
// Returns the removed message's subject and size.
// Lock should be held.
func (ms *memStore) removeMsgNoCB(seq uint64, secure bool) (subj string, size uint64, ok bool) {
sm, ok := ms.msgs[seq]
if !ok {
return _EMPTY_, 0, false
}
size = memStoreMsgSize(sm.subj, sm.hdr, sm.msg)
if ms.state.Msgs > 0 {
ms.state.Msgs--
if ss > ms.state.Bytes {
ss = ms.state.Bytes
if size > ms.state.Bytes {
size = ms.state.Bytes
}
ms.state.Bytes -= ss
ms.state.Bytes -= size
}
ms.dmap.Insert(seq)
ms.updateFirstSeq(seq)
@@ -2166,17 +2207,7 @@ func (ms *memStore) removeMsg(seq uint64, secure bool) bool {
// Must delete message after updating per-subject info, to be consistent with file store.
delete(ms.msgs, seq)
if ms.scb != nil {
// We do not want to hold any locks here.
ms.mu.Unlock()
if ms.scb != nil {
delta := int64(ss)
ms.scb(-1, -delta, seq, sm.subj)
}
ms.mu.Lock()
}
return ok
return sm.subj, size, true
}
// Type returns the type of the underlying store.
@@ -2595,13 +2626,15 @@ func (o *consumerMemStore) UpdateAcks(dseq, sseq uint64) error {
return ErrNoAckPolicy
}
// We do this regardless.
delete(o.state.Redelivered, sseq)
// On restarts the old leader may get a replay from the raft logs that are old.
if dseq <= o.state.AckFloor.Consumer {
return nil
}
if len(o.state.Pending) == 0 || o.state.Pending[sseq] == nil {
delete(o.state.Redelivered, sseq)
return ErrStoreMsgNotFound
}
@@ -2655,12 +2688,23 @@ func (o *consumerMemStore) UpdateAcks(dseq, sseq uint64) error {
}
}
}
// We do these regardless.
delete(o.state.Redelivered, sseq)
return nil
}
func (o *consumerMemStore) RemoveRedeliveredBelow(seq uint64) {
if seq == 0 {
return
}
o.mu.Lock()
defer o.mu.Unlock()
for s := range o.state.Redelivered {
if s < seq {
delete(o.state.Redelivered, s)
}
}
}
func (o *consumerMemStore) UpdateConfig(cfg *ConsumerConfig) error {
o.mu.Lock()
defer o.mu.Unlock()
+14 -6
View File
@@ -1271,10 +1271,14 @@ type Varz struct {
Routes int `json:"routes"` // Routes is the number of connected route servers
Remotes int `json:"remotes"` // Remotes is the configured route remote endpoints
Leafs int `json:"leafnodes"` // Leafs is the number connected leafnode clients
InMsgs int64 `json:"in_msgs"` // InMsgs is the number of messages this server received
OutMsgs int64 `json:"out_msgs"` // OutMsgs is the number of message this server sent
InBytes int64 `json:"in_bytes"` // InBytes is the number of bytes this server received
OutBytes int64 `json:"out_bytes"` // OutMsgs is the number of bytes this server sent
InMsgs int64 `json:"in_msgs"` // InMsgs is the total number of messages this server received. This includes messages from the clients, routers, gateways and leaf nodes
InBytes int64 `json:"in_bytes"` // InBytes is the total number of bytes this server received. This includes messages from the clients, routers, gateways and leaf nodes
InClientMsgs int64 `json:"in_client_msgs"` // InClientMsgs is the number of messages this server received from the clients
InClientBytes int64 `json:"in_client_bytes"` // InClientBytes is the number of bytes this server received from the clients
OutMsgs int64 `json:"out_msgs"` // OutMsgs is the total number of message this server sent. This includes messages sent to the clients, routers, gateways and leaf nodes
OutBytes int64 `json:"out_bytes"` // OutBytes is the total number of bytes this server sent. This includes messages sent to the clients, routers, gateways and leaf nodes
OutClientMsgs int64 `json:"out_client_msgs"` // OutClientMsgs is the number of messages this server sent to the clients
OutClientBytes int64 `json:"out_client_bytes"` // OutClientBytes is the number of bytes this server sent to the clients
SlowConsumers int64 `json:"slow_consumers"` // SlowConsumers is the total count of clients that were disconnected since start due to being slow consumers
StaleConnections int64 `json:"stale_connections"` // StaleConnections is the total count of stale connections that were detected
StalledClients int64 `json:"stalled_clients"` // StalledClients is the total number of times that clients have been stalled.
@@ -1877,6 +1881,10 @@ func (s *Server) updateVarzRuntimeFields(v *Varz, forceUpdate bool, pcpu float64
v.InBytes = atomic.LoadInt64(&s.inBytes)
v.OutMsgs = atomic.LoadInt64(&s.outMsgs)
v.OutBytes = atomic.LoadInt64(&s.outBytes)
v.InClientMsgs = atomic.LoadInt64(&s.inClientMsgs)
v.InClientBytes = atomic.LoadInt64(&s.inClientBytes)
v.OutClientMsgs = atomic.LoadInt64(&s.outClientMsgs)
v.OutClientBytes = atomic.LoadInt64(&s.outClientBytes)
v.SlowConsumers = atomic.LoadInt64(&s.slowConsumers)
v.StalledClients = atomic.LoadInt64(&s.stalls)
v.SlowConsumersStats = &SlowConsumersStats{
@@ -2511,7 +2519,7 @@ func (s *Server) AccountStatz(opts *AccountStatzOptions) (*AccountStatz, error)
s.accounts.Range(func(key, a any) bool {
acc := a.(*Account)
acc.mu.RLock()
if (opts != nil && opts.IncludeUnused) || acc.numLocalConnections() != 0 {
if (opts != nil && opts.IncludeUnused) || acc.numLocalConnections() != 0 || acc.numLocalLeafNodes() != 0 {
stz.Accounts = append(stz.Accounts, acc.statz())
}
acc.mu.RUnlock()
@@ -2522,7 +2530,7 @@ func (s *Server) AccountStatz(opts *AccountStatzOptions) (*AccountStatz, error)
if acc, ok := s.accounts.Load(a); ok {
acc := acc.(*Account)
acc.mu.RLock()
if opts.IncludeUnused || acc.numLocalConnections() != 0 {
if opts.IncludeUnused || acc.numLocalConnections() != 0 || acc.numLocalLeafNodes() != 0 {
stz.Accounts = append(stz.Accounts, acc.statz())
}
acc.mu.RUnlock()
+6 -3
View File
@@ -239,7 +239,7 @@ var (
errMQTTEmptyUsername = errors.New("empty user name not allowed")
errMQTTTopicIsEmpty = errors.New("topic cannot be empty")
errMQTTPacketIdentifierIsZero = errors.New("packet identifier cannot be 0")
errMQTTUnsupportedCharacters = errors.New("character ' ' not supported for MQTT topics")
errMQTTUnsupportedCharacters = errors.New("character not supported for MQTT topics")
errMQTTInvalidSession = errors.New("invalid MQTT session")
errMQTTInvalidRetainFlags = errors.New("invalid retained message flags")
errMQTTSessionCollision = errors.New("stored session does not match client ID")
@@ -5713,8 +5713,11 @@ func mqttToNATSSubjectConversion(mt []byte, wcOk bool) ([]byte, error) {
}
res = append(res, btsep)
}
case ' ':
// As of now, we cannot support ' ' in the MQTT topic/filter.
case ' ', '\t', '\n', '\r', '\f':
// We cannot support whitespace in the MQTT topic/filter — these
// characters would also corrupt the NATS wire protocol when the
// subject is forwarded to other connection types (e.g. leaf
// nodes) where the resulting control line could be split.
return nil, errMQTTUnsupportedCharacters
case 0x7f:
// SubjectTree uses DEL as an internal pivot marker, so retained
+138 -18
View File
@@ -22,7 +22,6 @@ import (
"iter"
"math"
"math/rand"
"net"
"os"
"path/filepath"
"runtime"
@@ -86,6 +85,7 @@ type RaftNode interface {
WaitForStop()
Delete()
IsDeleted() bool
Reset()
RecreateInternalSubs() error
IsSystemAccount() bool
GetTrafficAccountName() string
@@ -344,6 +344,7 @@ var (
errNoInternalClient = errors.New("raft: no internal client")
errMembershipChange = errors.New("raft: membership change in progress")
errRemoveLastNode = errors.New("raft: cannot remove the last peer")
errPeerNotFound = errors.New("raft: peer not found")
)
// This will bootstrap a raftNode by writing its config into the store directory.
@@ -374,16 +375,13 @@ func (s *Server) bootstrapRaftNode(cfg *RaftConfig, knownPeers []string, allPeer
if gw.Name == cn {
continue
}
for _, u := range gw.URLs {
host := u.Hostname()
// If this is an IP just add one.
if net.ParseIP(host) != nil {
ngwps++
} else {
addrs, _ := net.LookupHost(host)
ngwps += len(addrs)
}
}
// Each configured gateway URL represents one remote endpoint, so
// count it as a single peer. We must not resolve the host and add
// one per returned address: a hostname on a dual-stack host (e.g.
// "localhost" -> 127.0.0.1 + ::1) would then count the same server
// multiple times, inflating the expected meta-group size above the
// real node count and preventing meta leader election.
ngwps += len(gw.URLs)
}
if expected < nrs+ngwps {
@@ -1046,7 +1044,10 @@ func (n *raft) ProposeRemovePeer(peer string) error {
n.RUnlock()
return errMembershipChange
}
if _, ok := n.peers[peer]; !ok {
n.RUnlock()
return errPeerNotFound
}
if len(n.peers) <= 1 {
n.RUnlock()
return errRemoveLastNode
@@ -1383,6 +1384,11 @@ func (n *raft) installSnapshot(snap *snapshot) error {
return err
}
// If installing a snapshot past our commits, clear the cache.
if snap.lastIndex > n.commit && len(n.pae) > 0 {
n.pae = make(map[uint64]*appendEntry)
}
var state StreamState
n.wal.FastState(&state)
n.papplied = snap.lastIndex
@@ -1543,12 +1549,20 @@ func (c *checkpoint) InstallSnapshot(data []byte) (uint64, error) {
n.Unlock()
err := writeFileWithSync(c.snapFile, encoded, defaultFilePerms)
n.Lock()
// On either failure path, drop the file we just wrote so it doesn't get
// picked up by setupLastSnapshot on restart. Skip the remove if it's the
// snapshot already adopted into n.snapfile for this term/applied.
if err != nil {
if c.snapFile != n.snapfile {
os.Remove(c.snapFile)
}
// We could set write err here, but if this is a temporary situation, too many open files etc.
// we want to retry and snapshots are not fatal.
return 0, err
} else if !n.snapshotting {
// The checkpoint can be aborted at any time, don't continue if that happened.
if c.snapFile != n.snapfile {
os.Remove(c.snapFile)
}
return 0, errSnapAborted
}
@@ -1610,6 +1624,9 @@ func termAndIndexFromSnapFile(sn string) (term, index uint64, err error) {
if n, err := fmt.Sscanf(fn, snapFileT, &term, &index); err != nil || n != 2 {
return 0, 0, errBadSnapName
}
if fn != fmt.Sprintf(snapFileT, term, index) {
return 0, 0, errBadSnapName
}
return term, index, nil
}
@@ -2220,6 +2237,64 @@ func (n *raft) shutdown() {
}
}
// Reset discards this node's local raft state (log, snapshots, peer set,
// term/vote) so it can be caught up cleanly by another group with the same
// name. The caller is responsible for parking the node first (typically via
// SetObserver) if it should not compete for leadership immediately after;
// Reset itself steps the node down but does not flip observer mode.
func (n *raft) Reset() {
n.Lock()
defer n.Unlock()
n.debug("Resetting Raft state")
n.stepdownLocked(_EMPTY_)
// Cancel any in-flight catchup so it does not race the reset.
n.cancelCatchup()
// Drop proposals and inbound entries; they are no longer meaningful
// against whatever log this node ends up following.
n.prop.drain()
n.entry.drain()
n.resp.drain()
n.apply.drain()
n.reqs.drain()
n.votes.drain()
// Remove every snapshot under our snapshots dir, not just the one referenced
// by n.snapfile. Orphans (e.g. from a crash between install and the previous
// file's removal) would otherwise be picked up by setupLastSnapshot on the
// next restart and reseed the state we are discarding here.
snapDir := filepath.Join(n.sd, snapshotsDir)
if err := os.RemoveAll(snapDir); err != nil {
n.warn("Error removing snapshots directory during reset: %v", err)
}
if err := os.MkdirAll(snapDir, defaultDirPerms); err != nil {
n.warn("Error recreating snapshots directory during reset: %v", err)
}
n.snapfile = _EMPTY_
// Abort any inflight async snapshot checkpoint.
n.snapshotting = false
// Reset the WAL, but reset these first to not trip the assertion.
n.commit, n.hcommit, n.applied, n.processed, n.papplied = 0, 0, 0, 0, 0
n.resetWAL()
// Reset peer set to just ourselves; a new leader will fold us back into
// the cluster's membership view via processPeerState.
n.peers = map[string]*lps{n.id: {time.Time{}, 0, true}}
n.removed = nil
n.adjustClusterSizeAndQuorum()
n.term, n.vote = 0, _EMPTY_
n.writeTermVote()
// Persist the cleared peer state so a restart picks up the reset.
n.writePeerState(n.currentPeerStateLocked())
}
const (
raftAllSubj = "$NRG.>"
raftVoteSubj = "$NRG.V.%s"
@@ -2904,6 +2979,16 @@ func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _
n.RUnlock()
return
}
if _, ok := n.peers[string(msg)]; !ok {
n.debug("Ignoring forwarded peer removal proposal, peer not found")
n.RUnlock()
return
}
if len(n.peers) <= 1 {
n.debug("Ignoring forwarded peer removal proposal, remove last node")
n.RUnlock()
return
}
prop := n.prop
n.RUnlock()
@@ -2966,6 +3051,9 @@ func (n *raft) addPeer(peer string) {
// If we were on the removed list reverse that here.
if n.removed != nil {
delete(n.removed, peer)
if len(n.removed) == 0 {
n.removed = nil
}
}
if lp, ok := n.peers[peer]; !ok {
@@ -3901,6 +3989,19 @@ func (n *raft) truncateWAL(term, index uint64) {
// Set after we know we have truncated properly.
n.pterm, n.pindex = term, index
// Invalidate cached entries the WAL no longer has.
if index == 0 {
if len(n.pae) > 0 {
n.pae = make(map[uint64]*appendEntry)
}
} else {
for k := range n.pae {
if k > index {
delete(n.pae, k)
}
}
}
// Check if we're truncating an uncommitted membership change.
if n.membChangeIndex > 0 && n.membChangeIndex > index {
n.membChangeIndex = 0
@@ -4255,13 +4356,10 @@ func (n *raft) processAppendEntry(ae *appendEntry, sub *subscription) {
// Inherit state from appendEntry with the leader's snapshot.
hadPreviousSnapshot := n.snapfile != _EMPTY_
n.pindex = ae.pindex
n.pterm = ae.pterm
n.commit = ae.pindex
snap := &snapshot{
lastTerm: n.pterm,
lastIndex: n.pindex,
lastTerm: ae.pterm,
lastIndex: ae.pindex,
peerstate: encodePeerState(&peerState{n.peerNames(), n.csz, n.extSt}),
data: ae.entries[0].Data,
}
@@ -4271,6 +4369,9 @@ func (n *raft) processAppendEntry(ae *appendEntry, sub *subscription) {
n.Unlock()
return
}
n.pindex = ae.pindex
n.pterm = ae.pterm
n.commit = ae.pindex
n.resetInitializing()
if !hadPreviousSnapshot {
@@ -4422,9 +4523,28 @@ func (n *raft) processPeerState(ps *peerState) {
if lp := old[peer]; lp != nil {
lp.kp = true
n.peers[peer] = lp
delete(old, peer)
} else {
n.peers[peer] = &lps{time.Time{}, 0, true}
}
// If we were on the removed list reverse that here.
if n.removed != nil {
delete(n.removed, peer)
if len(n.removed) == 0 {
n.removed = nil
}
}
}
// Any remaining old nodes are marked as removed, so they can't be
// re-added via automatic peer tracking.
if len(old) > 0 {
if n.removed == nil {
n.removed = map[string]time.Time{}
}
now := time.Now()
for peer := range old {
n.removed[peer] = now
}
}
n.debug("Update peers from leader to %+v", n.peers)
n.writePeerState(ps)
+25 -27
View File
@@ -2015,7 +2015,7 @@ func (s *Server) createRoute(conn net.Conn, rURL *url.URL, rtype RouteType, goss
pingInterval = opts.Cluster.PingInterval
}
if opts.Cluster.MaxPingsOut > 0 {
pingMax = opts.MaxPingsOut
pingMax = opts.Cluster.MaxPingsOut
}
c.watchForStaleConnection(adjustPingInterval(ROUTER, pingInterval), pingMax)
} else {
@@ -2539,21 +2539,21 @@ func (s *Server) updateRouteSubscriptionMap(acc *Account, sub *subscription, del
// queue subscriptions updates (sub/unsub).
// See https://github.com/nats-io/nats-server/pull/1126 for more details.
if isq {
acc.sqmu.Lock()
acc.smu.Lock()
}
acc.mu.Lock()
}
accUnlock := func() {
acc.mu.Unlock()
if isq {
acc.sqmu.Unlock()
acc.smu.Unlock()
}
}
accLock()
// This is non-nil when we know we are in cluster mode.
rm, lqws := acc.rm, acc.lqws
rm, lws := acc.rm, acc.lws
if rm == nil {
accUnlock()
return
@@ -2573,9 +2573,7 @@ func (s *Server) updateRouteSubscriptionMap(acc *Account, sub *subscription, del
n += delta
if n <= 0 {
delete(rm, key)
if isq {
delete(lqws, key)
}
delete(lws, key)
update = true // Update for deleting (N->0)
} else {
rm[key] = n
@@ -2650,29 +2648,29 @@ func (s *Server) updateRouteSubscriptionMap(acc *Account, sub *subscription, del
trace := atomic.LoadInt32(&s.logging.trace) == 1
s.mu.RUnlock()
// If we are a queue subscriber we need to make sure our updates are serialized from
// potential multiple connections. We want to make sure that the order above is preserved
// here but not necessarily all updates need to be sent. We need to block and recheck the
// n count with the lock held through sending here. We will suppress duplicate sends of same qw.
if isq {
// However, we can't hold the acc.mu lock since we allow client.mu.Lock -> acc.mu.Lock
// but not the opposite. So use a dedicated lock while holding the route's lock.
acc.sqmu.Lock()
defer acc.sqmu.Unlock()
// We need to make sure our updates are serialized from potential multiple connections. We want
// to make sure that the order above is preserved here but not necessarily all updates need to
// be sent. We need to block and recheck the n count with the lock held through sending here.
//
// However, we can't hold the acc.mu lock since we allow client.mu.Lock -> acc.mu.Lock
// but not the opposite. So use a dedicated lock while holding the route's lock.
acc.smu.Lock()
defer acc.smu.Unlock()
acc.mu.Lock()
n = rm[key]
acc.mu.Lock()
n = rm[key]
if isq {
sub.qw = n
// Check the last sent weight here. If same, then someone
// beat us to it and we can just return here. Otherwise update
if ls, ok := lqws[key]; ok && ls == n {
acc.mu.Unlock()
return
} else if n > 0 {
lqws[key] = n
}
acc.mu.Unlock()
}
// Check the last sent value here. If same, then someone beat us to it and
// we can just return here. Otherwise update.
if ls, ok := lws[key]; ok && ls == n {
acc.mu.Unlock()
return
} else if n > 0 {
lws[key] = n
}
acc.mu.Unlock()
// Snapshot into array
subs := []*subscription{sub}
+21 -11
View File
@@ -402,9 +402,13 @@ type nodeInfo struct {
type stats struct {
inMsgs int64
outMsgs int64
inBytes int64
inClientMsgs int64
inClientBytes int64
outMsgs int64
outBytes int64
outClientMsgs int64
outClientBytes int64
slowConsumers int64
staleConnections int64
stalls int64
@@ -1964,7 +1968,7 @@ func (s *Server) registerAccountNoLock(acc *Account) *Account {
// TODO(dlc)- Double check that we need this for GWs.
if acc.rm == nil && s.opts != nil && s.shouldTrackSubscriptions() {
acc.rm = make(map[string]int32)
acc.lqws = make(map[string]int32)
acc.lws = make(map[string]int32)
}
acc.srv = s
acc.updated = time.Now()
@@ -2555,6 +2559,10 @@ func (s *Server) Shutdown() {
if s == nil {
return
}
// Prevent issues with multiple calls.
if !s.shutdown.CompareAndSwap(false, true) {
return
}
// This is for JetStream R1 Pull Consumers to allow signaling
// that pending pull requests are invalid.
s.signalPullConsumers()
@@ -2568,11 +2576,6 @@ func (s *Server) Shutdown() {
// eventing items associated with accounts.
s.shutdownEventing()
// Prevent issues with multiple calls.
if s.isShuttingDown() {
return
}
s.mu.Lock()
s.Noticef("Initiating Shutdown...")
@@ -2580,7 +2583,6 @@ func (s *Server) Shutdown() {
opts := s.getOpts()
s.shutdown.Store(true)
s.running.Store(false)
s.grMu.Lock()
s.grRunning = false
@@ -3411,8 +3413,12 @@ func (s *Server) createClientEx(conn net.Conn, inProcess bool) *client {
}
}
// Check for proxy protocol if enabled.
if !isClosed && !tlsRequired && opts.ProxyProtocol {
// Check for proxy protocol if enabled. The PROXY header is sent as
// plaintext before any TLS handshake per the spec, so we must read it
// before doing TLS even when TLS is required. Any bytes read past the
// header are kept in `pre` and replayed into the TLS handshake (or the
// non-TLS protocol parser) by the tlsMixConn wrapper used below.
if !isClosed && opts.ProxyProtocol {
if len(pre) == 0 {
// There has been no pre-read yet, do so so we can work out
// if the client is trying to negotiate PROXY.
@@ -3640,7 +3646,11 @@ func tlsTimeout(c *client, conn *tls.Conn) {
}
cs := conn.ConnectionState()
if !cs.HandshakeComplete {
c.Errorf("TLS handshake timeout")
if c.kind == CLIENT || c.kind == LEAF {
c.Debugf("TLS handshake timeout")
} else {
c.Errorf("TLS handshake timeout")
}
c.sendErr("Secure Connection - TLS Required")
c.closeConnection(TLSHandshakeError)
}
+1
View File
@@ -364,6 +364,7 @@ type ConsumerStore interface {
HasState() bool
UpdateDelivered(dseq, sseq, dc uint64, ts int64) error
UpdateAcks(dseq, sseq uint64) error
RemoveRedeliveredBelow(seq uint64)
UpdateConfig(cfg *ConsumerConfig) error
Update(*ConsumerState) error
ForceUpdate(*ConsumerState) error
+145 -67
View File
@@ -264,7 +264,7 @@ type PubAck struct {
Duplicate bool `json:"duplicate,omitempty"`
Value string `json:"val,omitempty"`
BatchId string `json:"batch,omitempty"`
BatchSize int `json:"count,omitempty"`
BatchSize uint64 `json:"count,omitempty"`
}
// CounterValue is the body of a message when used as a counter.
@@ -571,6 +571,7 @@ type stream struct {
mirrorLastBySub *subscription // Mirrors only.
monitorWg sync.WaitGroup // Wait group for the monitor routine.
monitorMu sync.Mutex // Serializes monitorWg's Add against Wait to prevent a WaitGroup reuse panic.
// If standalone/single-server, the offline reason needs to be stored directly in the stream.
// Otherwise, if clustered it will be part of the stream assignment.
@@ -582,8 +583,9 @@ type stream struct {
// inflightSubjectRunningTotal stores a running total of inflight messages for a specific subject.
type inflightSubjectRunningTotal struct {
bytes uint64 // Running total of inflight bytes for inflight messages.
ops uint64 // Inflight operations, i.e. inflight messages for this subject. If this reaches zero, we can remove the running total.
bytes uint64 // Running total of inflight bytes for inflight messages.
ops uint64 // Inflight operations, i.e. inflight messages for this subject. If this reaches zero, we can remove the running total.
schedule bool // Marks whether the last message is a schedule.
}
// msgCounterRunningTotal stores a running total and a number of inflight
@@ -816,7 +818,7 @@ func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileSt
if isClustered {
_, reserved = js.tieredStreamAndReservationCount(a.Name, tier, cfg)
}
if err := js.checkAllLimits(&selected, cfg, reserved, 0); err != nil {
if err := js.checkAllLimits(&selected, tier, cfg, reserved, 0); err != nil {
js.mu.RUnlock()
return nil, err
}
@@ -1750,6 +1752,9 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo
if cfg.AllowMsgTTL {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use message TTLs"))
}
if cfg.AllowMsgSchedules {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use message schedules"))
}
if cfg.Retention != LimitsPolicy {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream can only use limits retention"))
}
@@ -1786,6 +1791,9 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo
}
if cfg.AllowMsgSchedules {
if cfg.Discard == DiscardNew {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("message scheduling cannot use discard new"))
}
if !cfg.AllowRollup {
if pedantic {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("message scheduling cannot be set if roll-ups are disabled"))
@@ -2340,11 +2348,10 @@ func (jsa *jsAccount) configUpdateCheck(old, new *StreamConfig, s *Server, pedan
// Save the user configured MaxBytes.
newMaxBytes := cfg.MaxBytes
maxBytesOffset := int64(0)
// We temporarily set cfg.MaxBytes to maxBytesDiff because checkAllLimits
// adds cfg.MaxBytes to the current reserved limit and checks if we've gone
// over. However, we don't want an addition cfg.MaxBytes, we only want to
// over. However, we don't want an additional cfg.MaxBytes, we only want to
// reserve the difference between the new and the old values.
cfg.MaxBytes = maxBytesDiff
@@ -2371,15 +2378,13 @@ func (jsa *jsAccount) configUpdateCheck(old, new *StreamConfig, s *Server, pedan
if isClustered {
_, reserved = js.tieredStreamAndReservationCount(acc.Name, tier, &cfg)
}
// reservation does not account for this stream, hence add the old value
if old.MaxBytes > 0 {
if tier == _EMPTY_ && old.Replicas > 1 {
reserved = addSaturate(reserved, mulSaturate(int64(old.Replicas), old.MaxBytes))
} else {
reserved = addSaturate(reserved, old.MaxBytes)
}
}
if err := js.checkAllLimits(&selected, &cfg, reserved, maxBytesOffset); err != nil {
// reserved covers only the other streams. checkAllLimits adds this stream's
// footprint via cfg.MaxBytes, which is currently maxBytesDiff, so it only
// adds the diff. Add the remaining (newMaxBytes - maxBytesDiff) here so the
// two together equal this stream's true new footprint, even when Replicas
// changes on update.
reserved = addSaturate(reserved, accountReservation(tier, cfg.Replicas, newMaxBytes-maxBytesDiff))
if err := js.checkAllLimits(&selected, tier, &cfg, reserved, 0); err != nil {
return nil, err
}
// Restore the user configured MaxBytes.
@@ -3018,14 +3023,20 @@ func (mset *stream) retryDisconnectedSyncConsumers() {
clientClosed := func(c *client) bool {
return c != nil && (c.flags.isSet(closeConnection) || c.flags.isSet(connMarkedClosed))
}
// Stale sources need to be reset: we expect a heartbeat every sourceHealthHB, so missing a couple
// is a strong signal the remote delivery is no longer reaching us and a retry is warranted.
// Stale sources need to be reset: if not seen past the health check interval, it's stale.
stale := func(si *sourceInfo) bool {
return time.Since(time.Unix(0, si.last.Load())) > 2*sourceHealthHB
return time.Since(time.Unix(0, si.last.Load())) > sourceHealthCheckInterval
}
shouldRetry := func(si *sourceInfo) bool {
if si != nil && (si.sip || si.sub == nil || clientClosed(si.sub.client) || stale(si)) {
si.fails, si.sip = 0, false
if si != nil && !si.sip && (si.sub == nil || clientClosed(si.sub.client) || stale(si)) {
// Skip if a recreate is already scheduled and we can't cancel it.
if t, ok := mset.sourceSetupSchedules[si.iname]; ok {
if !t.Stop() {
return false
}
delete(mset.sourceSetupSchedules, si.iname)
}
si.fails = 0
mset.cancelSourceInfo(si)
return true
}
@@ -3205,7 +3216,13 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool {
} else {
// If the deliver sequence matches then the upstream stream has expired or deleted messages.
if dseq == mset.mirror.dseq+1 {
mset.skipMsgs(mset.mirror.sseq+1, sseq-1)
if err := mset.skipMsgs(mset.mirror.sseq+1, sseq-1); err != nil {
mset.mirror.sseq = osseq
mset.mirror.dseq = odseq
mset.mu.Unlock()
mset.retryMirrorConsumer()
return false
}
mset.mirror.dseq++
mset.mirror.sseq = sseq
} else {
@@ -3263,7 +3280,7 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool {
if err != nil {
if strings.Contains(err.Error(), "no space left") {
s.Errorf("JetStream out of space, will be DISABLED")
s.DisableJetStream()
s.ShutdownJetStream()
return false
}
if err != errLastSeqMismatch {
@@ -3274,20 +3291,18 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool {
accName, sname, err)
} else {
// We may have missed messages, restart.
if lseq := mset.lastSeq(); sseq <= lseq {
mset.mu.Lock()
lseq := mset.lastSeq()
mset.mu.Lock()
if mset.mirror != nil {
mset.mirror.lag = olag
mset.mirror.sseq = lseq
mset.mirror.dseq = odseq
mset.mu.Unlock()
return false
} else {
mset.mu.Lock()
mset.mirror.dseq = odseq
mset.mirror.sseq = osseq
mset.mu.Unlock()
mset.retryMirrorConsumer()
if sseq <= lseq {
mset.mirror.sseq = lseq
}
}
mset.mu.Unlock()
mset.retryMirrorConsumer()
}
}
return err == nil
@@ -3324,20 +3339,21 @@ func (mset *stream) retryMirrorConsumer() error {
}
// Lock should be held.
func (mset *stream) skipMsgs(start, end uint64) {
func (mset *stream) skipMsgs(start, end uint64) error {
node, store := mset.node, mset.store
// If we are not clustered we can short circuit now with store.SkipMsgs
if node == nil {
store.SkipMsgs(start, end-start+1)
if err := store.SkipMsgs(start, end-start+1); err != nil {
return err
}
mset.lseq = end
return
return nil
}
// Must only be enabled once every peer in the cluster supports receiving
// deleteRangeOp in the normal apply path; older peers panic on unknown ops.
if mset.srv.getOpts().getFeatureFlag(FeatureFlagJsRaftDeleteRange) {
node.Propose(encodeDeleteRange(&DeleteRange{First: start, Num: end - start + 1}))
return
return node.Propose(encodeDeleteRange(&DeleteRange{First: start, Num: end - start + 1}))
}
var entries []*Entry
@@ -3345,7 +3361,9 @@ func (mset *stream) skipMsgs(start, end uint64) {
entries = append(entries, newEntry(EntryNormal, encodeStreamMsg(_EMPTY_, _EMPTY_, nil, nil, seq-1, 0, false)))
// So a single message does not get too big.
if len(entries) > 10_000 {
node.ProposeMulti(entries)
if err := node.ProposeMulti(entries); err != nil {
return err
}
// We need to re-create `entries` because there is a reference
// to it in the node's pae map.
entries = entries[:0]
@@ -3353,8 +3371,9 @@ func (mset *stream) skipMsgs(start, end uint64) {
}
// Send all at once.
if len(entries) > 0 {
node.ProposeMulti(entries)
return node.ProposeMulti(entries)
}
return nil
}
const (
@@ -3718,13 +3737,25 @@ func (mset *stream) setupMirrorConsumer() error {
state = StreamState{}
mset.store.FastState(&state)
if state.LastSeq < ccr.ConsumerInfo.Delivered.Stream {
// Local helper: abort consumer setup, leaving the mirror in its
// pre-setup state so the retry path can re-create it cleanly.
failSetup := func(setupErr error) {
mset.cancelSourceInfo(mirror)
mirror.err = NewJSMirrorConsumerSetupFailedError(setupErr, Unless(setupErr))
retry = true
mset.mu.Unlock()
}
// Check to see if delivered is past our last and we have no msgs. This will help the
// case when mirroring a stream that has a very high starting sequence number.
if state.Msgs == 0 && ccr.ConsumerInfo.Delivered.Stream > state.LastSeq {
mset.store.PurgeEx(_EMPTY_, ccr.ConsumerInfo.Delivered.Stream+1, 0)
if _, err := mset.store.PurgeEx(_EMPTY_, ccr.ConsumerInfo.Delivered.Stream+1, 0); err != nil {
failSetup(err)
return
}
mset.lseq = ccr.ConsumerInfo.Delivered.Stream
} else {
mset.skipMsgs(state.LastSeq+1, ccr.ConsumerInfo.Delivered.Stream)
} else if err := mset.skipMsgs(state.LastSeq+1, ccr.ConsumerInfo.Delivered.Stream); err != nil {
failSetup(err)
return
}
}
@@ -4399,7 +4430,7 @@ func (mset *stream) processInboundSourceMsg(si *sourceInfo, m *inMsg) bool {
s := mset.srv
if strings.Contains(err.Error(), "no space left") {
s.Errorf("JetStream out of space, will be DISABLED")
s.DisableJetStream()
s.ShutdownJetStream()
} else {
mset.mu.RLock()
accName, sname, iName := mset.acc.Name, mset.cfg.Name, si.iname
@@ -5019,10 +5050,14 @@ func (mset *stream) deleteAtomicBatches(shuttingDown bool) {
// Lock should be held.
func (mset *stream) deleteBatchApplyState() {
if batch := mset.batchApply; batch != nil {
// Need to return entries (if any) to the pool.
// Clear under batch.mu so a stale reference held by the stream monitor
// can't re-pool entries we already returned.
batch.mu.Lock()
for _, bce := range batch.entries {
bce.ReturnToPool()
}
batch.clearBatchStateLocked()
batch.mu.Unlock()
mset.batchApply = nil
}
}
@@ -5176,9 +5211,12 @@ func (mset *stream) storeUpdates(md, bd int64, seq uint64, subj string) {
mset.clsMu.RUnlock()
} else if md < 0 {
// Batch decrements we need to force consumers to re-calculate num pending.
var ss StreamState
mset.store.FastState(&ss)
mset.clsMu.RLock()
for _, o := range mset.cList {
o.streamNumPendingLocked()
o.removeRedeliveredBelow(ss.FirstSeq)
}
mset.clsMu.RUnlock()
}
@@ -5558,15 +5596,14 @@ func getFastBatch(reply string, hdr []byte) (*FastBatch, bool) {
if o = strings.LastIndexByte(reply[:o], '.'); o == -1 {
return nil, true
}
a := parseInt64(stringToBytes(reply[o+1 : p]))
if a < 1 {
seq, ok := parseUint64(stringToBytes(reply[o+1 : p]))
// Reject math.MaxUint64 to prevent b.lseq overflowing on the next b.lseq++.
if !ok || seq == 0 || seq == math.MaxUint64 {
return nil, true
}
b.seq = uint64(a)
b.seq = seq
p = o
if b.seq <= 0 {
return nil, true
} else if b.seq == 1 && b.commitEob {
if b.seq == 1 && b.commitEob {
return nil, true
}
if op == FastBatchOpStart && b.seq != 1 {
@@ -5590,7 +5627,7 @@ func getFastBatch(reply string, hdr []byte) (*FastBatch, bool) {
if o = strings.LastIndexByte(reply[:o], '.'); o == -1 {
return nil, true
}
a = parseInt64(stringToBytes(reply[o+1 : p]))
a := parseInt64(stringToBytes(reply[o+1 : p]))
if a <= 0 {
a = 10
} else if a > math.MaxUint16 {
@@ -5613,7 +5650,7 @@ func getBatchSequence(hdr []byte) (uint64, bool) {
if len(bseq) == 0 {
return 0, false
}
return uint64(parseInt64(bseq)), true
return parseUint64(bseq)
}
// Signal if we are clustered. Will acquire rlock.
@@ -6528,6 +6565,23 @@ func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg
outq.sendMsg(reply, b)
}
return apiErr
} else {
// Check that the to-be-purged subject is a schedule message.
// We still allow this message through if there exists no message for this subject,
// to remain backward-compatible. An "expected at sequence" check can still be
// performed to make this stricter.
var smv StoreMsg
sm, _ := store.LoadLastMsg(bytesToString(scheduler), &smv)
if sm != nil && len(sliceHeader(JSSchedulePattern, sm.hdr)) == 0 {
apiErr := NewJSMessageSchedulesSchedulerInvalidError()
if canRespond {
resp.PubAck = &PubAck{Stream: name}
resp.Error = apiErr
b, _ := json.Marshal(resp)
outq.sendMsg(reply, b)
}
return apiErr
}
}
} else if !sourced && len(sliceHeader(JSScheduler, hdr)) > 0 {
// Clients may only use Nats-Scheduler alongside Nats-Schedule-Next.
@@ -6842,6 +6896,10 @@ func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg
var thdrsOnly bool
if mset.tr != nil {
tsubj, _ = mset.tr.Match(subject)
if tsubj != _EMPTY_ && !IsValidPublishSubject(tsubj) {
s.RateLimitWarnf("Stream '%s > %s' suppressing republish with invalid subject %q", accName, name, tsubj)
tsubj = _EMPTY_ // ... stops the republish.
}
if mset.cfg.RePublish != nil {
thdrsOnly = mset.cfg.RePublish.HeadersOnly
}
@@ -6910,7 +6968,7 @@ func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg
if isPermissionError(err) {
// messages in block cache could be lost in the worst case.
// In the clustered mode it is very highly unlikely as a result of replication.
go mset.srv.DisableJetStream()
go mset.srv.ShutdownJetStream()
mset.srv.Warnf("Filesystem permission denied while writing msg, disabling JetStream: %v", err)
return err
}
@@ -8162,10 +8220,7 @@ func (mset *stream) resetAndWaitOnConsumers() {
node.StepDown()
node.Stop()
}
if o.isMonitorRunning() {
o.signalMonitorQuit()
o.monitorWg.Wait()
}
o.stopMonitoring()
}
}
@@ -8269,8 +8324,7 @@ func (mset *stream) stop(deleteFlag, advisory bool) error {
// but should we log?
o.stopWithFlags(deleteFlag, deleteFlag, false, advisory)
if !isShuttingDown {
o.signalMonitorQuit()
o.monitorWg.Wait()
o.stopMonitoring()
}
}
}
@@ -8515,14 +8569,16 @@ func (mset *stream) setConsumer(o *consumer) {
// Lock should be held.
func (mset *stream) removeConsumer(o *consumer) {
if o.cfg.FilterSubject != _EMPTY_ && mset.numFilter > 0 {
mset.numFilter--
}
if (o.cfg.Direct || o.cfg.Sourcing) && mset.sourcingConsumers > 0 {
mset.sourcingConsumers--
}
if mset.consumers != nil {
if _, ok := mset.consumers[o.name]; ok {
delete(mset.consumers, o.name)
if o.cfg.FilterSubject != _EMPTY_ && mset.numFilter > 0 {
mset.numFilter--
}
if (o.cfg.Direct || o.cfg.Sourcing) && mset.sourcingConsumers > 0 {
mset.sourcingConsumers--
}
// Now update consumers list as well
mset.clsMu.Lock()
for i, ol := range mset.cList {
@@ -9000,7 +9056,7 @@ func (a *Account) RestoreStream(ncfg *StreamConfig, r io.Reader) (*stream, error
}
bc += hdr.Size
js.mu.RLock()
err = js.checkAllLimits(&selected, &cfg, reserved, bc)
err = js.checkAllLimits(&selected, tier, &cfg, reserved, bc)
js.mu.RUnlock()
if err != nil {
return nil, err
@@ -9176,6 +9232,28 @@ func (mset *stream) checkConsumerReplication() {
}
}
// startMonitorWg registers a pending monitor goroutine on monitorWg. It is
// held under monitorMu so that the monitorWg.Add can never race a concurrent
// monitorWg.Wait in stopMonitoring. The corresponding monitorWg.Done is done by
// the monitor goroutine directly and must not be wrapped with monitorMu.
func (mset *stream) startMonitorWg() {
mset.monitorMu.Lock()
mset.monitorWg.Add(1)
mset.monitorMu.Unlock()
}
// stopMonitoring signals any running monitor goroutine to quit and waits for
// it to fully exit.
func (mset *stream) stopMonitoring() {
// monitorMu is held across both the quit signal and the wait so that a
// concurrent startMonitorWg cannot slip a new monitor generation in
// between.
mset.monitorMu.Lock()
defer mset.monitorMu.Unlock()
mset.signalMonitorQuit()
mset.monitorWg.Wait()
}
// Will check if we are running in the monitor already and if not set the appropriate flag.
func (mset *stream) checkInMonitor() bool {
mset.mu.Lock()
+8 -5
View File
@@ -485,7 +485,7 @@ func LazyIntersect[TL, TR any](tl *SubjectTree[TL], tr *SubjectTree[TR], cb func
// IntersectGSL will match all items in the given subject tree that
// have interest expressed in the given sublist. The callback will only be called
// once for each subject, regardless of overlapping subscriptions in the sublist.
func IntersectGSL[T any, SL comparable](t *SubjectTree[T], sl *gsl.GenericSublist[SL], cb func(subject []byte, val *T)) {
func IntersectGSL[T any, SL comparable](t *SubjectTree[T], sl *gsl.GenericSublist[SL], cb func(subject []byte, val *T) bool) {
if t == nil || t.root == nil || sl == nil {
return
}
@@ -493,14 +493,14 @@ func IntersectGSL[T any, SL comparable](t *SubjectTree[T], sl *gsl.GenericSublis
_intersectGSL(t.root, _pre[:0], sl, cb)
}
func _intersectGSL[T any, SL comparable](n node, pre []byte, sl *gsl.GenericSublist[SL], cb func(subject []byte, val *T)) {
func _intersectGSL[T any, SL comparable](n node, pre []byte, sl *gsl.GenericSublist[SL], cb func(subject []byte, val *T) bool) bool {
if n.isLeaf() {
ln := n.(*leaf[T])
subj := append(pre, ln.suffix...)
if sl.HasInterest(bytesToString(subj)) {
cb(subj, &ln.value)
return cb(subj, &ln.value)
}
return
return true
}
bn := n.base()
pre = append(pre, bn.prefix...)
@@ -512,8 +512,11 @@ func _intersectGSL[T any, SL comparable](n node, pre []byte, sl *gsl.GenericSubl
if !hasInterestForTokens(sl, subj, len(pre)) {
continue
}
_intersectGSL(cn, pre, sl, cb)
if !_intersectGSL(cn, pre, sl, cb) {
return false
}
}
return true
}
// The subject tree can return partial tokens so we need to check starting interest
+5 -4
View File
@@ -638,9 +638,10 @@ func (s *Sublist) hasInterest(subject string, doLock bool, np, nq *int) bool {
if doLock {
s.RLock()
}
var matched bool
var matched, ok bool
if s.cache != nil {
if r, ok := s.cache[subject]; ok {
var r *SublistResult
if r, ok = s.cache[subject]; ok {
if np != nil && nq != nil {
*np += len(r.psubs)
for _, qsub := range r.qsubs {
@@ -653,9 +654,9 @@ func (s *Sublist) hasInterest(subject string, doLock bool, np, nq *int) bool {
if doLock {
s.RUnlock()
}
if matched {
if ok {
atomic.AddUint64(&s.cacheHits, 1)
return true
return matched
}
tsa := [32]string{}
+20
View File
@@ -124,6 +124,26 @@ func parseInt64(d []byte) (n int64) {
return n
}
// parseUint64 expects decimal positive numbers. Returns the value and true on success,
// or 0 and false on invalid input or overflow.
func parseUint64(d []byte) (uint64, bool) {
if len(d) == 0 {
return 0, false
}
var n uint64
for _, dec := range d {
if dec < asciiZero || dec > asciiNine {
return 0, false
}
digit := uint64(dec) - asciiZero
if n > math.MaxUint64/10 || (n == math.MaxUint64/10 && digit > math.MaxUint64%10) {
return 0, false
}
n = n*10 + digit
}
return n, true
}
// Helper to move from float seconds to time.Duration
func secondsToDuration(seconds float64) time.Duration {
ttl := seconds * float64(time.Second)
+6 -12
View File
@@ -1,4 +1,4 @@
// Copyright 2020-2025 The NATS Authors
// Copyright 2020-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -16,7 +16,6 @@ package server
import (
"bytes"
crand "crypto/rand"
"crypto/sha1"
"crypto/tls"
"encoding/base64"
"encoding/binary"
@@ -1107,15 +1106,6 @@ func wsGetHostAndPort(tls bool, hostport string) (string, string, error) {
return strings.ToLower(host), port, err
}
// Concatenate the key sent by the client with the GUID, then computes the SHA1 hash
// and returns it as a based64 encoded string.
func wsAcceptKey(key string) string {
h := sha1.New()
h.Write([]byte(key))
h.Write(wsGUID)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func wsMakeChallengeKey() (string, error) {
p := make([]byte, 16)
if _, err := io.ReadFull(crand.Reader, p); err != nil {
@@ -1131,6 +1121,9 @@ func validateWebsocketOptions(o *Options) error {
if wo.Port == 0 {
return nil
}
if !wsAllowedFIPS() {
return fmt.Errorf("websocket: cannot be used in FIPS-140 mode when built with this Go version, use Go 1.26 or later")
}
// Enforce TLS... unless NoTLS is set to true.
if wo.TLSConfig == nil && !wo.NoTLS {
return errors.New("websocket requires TLS configuration")
@@ -1560,10 +1553,11 @@ func (c *client) wsCollapsePtoNB() (net.Buffers, int64) {
if mask {
wsMaskBuf(key, p[:lp])
}
bufs = append(bufs, fh[:n], p[:lp])
bufs = append(bufs, fh[:n], append(nbPoolGet(lp), p[:lp]...))
csz += n + lp
p = p[lp:]
}
nbPoolPut(b)
} else {
ol := len(p)
h, key := wsCreateFrameHeader(mask, true, wsBinaryMessage, ol)
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !go1.26
package server
import (
"crypto/fips140"
"crypto/sha1"
"encoding/base64"
)
func wsAllowedFIPS() bool {
// SHA-1 is not permitted on Go 1.25 FIPS builds because we cannot avoid its
// enforcement for Sec-WebSocket-Key and Sec-WebSocket-Accept, it will result
// in a panic.
return !fips140.Enabled()
}
// Concatenate the key sent by the client with the GUID, then computes the SHA1 hash
// and returns it as a based64 encoded string.
func wsAcceptKey(key string) string {
h := sha1.New()
h.Write([]byte(key))
h.Write(wsGUID)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build go1.26
package server
import (
"crypto/fips140"
"crypto/sha1"
"encoding/base64"
)
func wsAllowedFIPS() bool {
// As SHA-1 is only used for Sec-WebSocket-Key and Sec-WebSocket-Accept, we
// can continue to allow it in FIPS builds as long as they are built with
// Go 1.26 or later only.
return true
}
// Concatenate the key sent by the client with the GUID, then computes the SHA1 hash
// and returns it as a based64 encoded string.
func wsAcceptKey(key string) string {
var r []byte
fips140.WithoutEnforcement(func() {
h := sha1.New()
h.Write([]byte(key))
h.Write(wsGUID)
r = h.Sum(nil)
})
return base64.StdEncoding.EncodeToString(r)
}
+3 -7
View File
@@ -19,7 +19,7 @@ package nkeys
import "io"
// Version is our current version
const Version = "0.4.7"
const Version = "0.4.16"
// KeyPair provides the central interface to nkeys.
type KeyPair interface {
@@ -66,15 +66,11 @@ func CreateOperator() (KeyPair, error) {
// FromPublicKey will create a KeyPair capable of verifying signatures.
func FromPublicKey(public string) (KeyPair, error) {
raw, err := decode([]byte(public))
prefix, key, err := decodePublicKey(public)
if err != nil {
return nil, err
}
pre := PrefixByte(raw[0])
if err := checkValidPublicPrefixByte(pre); err != nil {
return nil, ErrInvalidPublicKey
}
return &pub{pre, raw[1:]}, nil
return &pub{prefix, key}, nil
}
// FromSeed will create a KeyPair capable of signing and verifying signatures.
+3
View File
@@ -52,6 +52,9 @@ func (p *pub) Sign(input []byte) ([]byte, error) {
// Verify will verify the input against a signature utilizing the public key.
func (p *pub) Verify(input []byte, sig []byte) error {
if len(p.pub) != ed25519.PublicKeySize {
return ErrInvalidPublicKey
}
if !ed25519.Verify(p.pub, input, sig) {
return ErrInvalidSignature
}
+18 -5
View File
@@ -15,6 +15,7 @@ package nkeys
import (
"bytes"
"crypto/ed25519"
"encoding/base32"
"encoding/binary"
)
@@ -184,6 +185,9 @@ func DecodeSeed(src []byte) (PrefixByte, []byte, error) {
if checkValidPublicPrefixByte(PrefixByte(b2)) != nil {
return PrefixByteSeed, nil, ErrInvalidSeed
}
if len(raw[2:]) != seedLen {
return PrefixByteSeed, nil, ErrInvalidSeed
}
return PrefixByte(b2), raw[2:], nil
}
@@ -208,14 +212,23 @@ func Prefix(src string) PrefixByte {
// IsValidPublicKey will decode and verify that the string is a valid encoded public key.
func IsValidPublicKey(src string) bool {
b, err := decode([]byte(src))
_, _, err := decodePublicKey(src)
return err == nil
}
func decodePublicKey(public string) (PrefixByte, []byte, error) {
raw, err := decode([]byte(public))
if err != nil {
return false
return PrefixByteUnknown, nil, err
}
if prefix := PrefixByte(b[0]); checkValidPublicPrefixByte(prefix) != nil {
return false
pre := PrefixByte(raw[0])
if err := checkValidPublicPrefixByte(pre); err != nil {
return PrefixByteUnknown, nil, ErrInvalidPublicKey
}
return true
if len(raw[1:]) != ed25519.PublicKeySize {
return PrefixByteUnknown, nil, ErrInvalidPublicKey
}
return pre, raw[1:], nil
}
// IsValidPublicUserKey will decode and verify the string is a valid encoded Public User Key.
-11
View File
@@ -1,11 +0,0 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.25
package blake2b
import "hash"
var _ hash.XOF = (*xof)(nil)
+3 -3
View File
@@ -20,7 +20,7 @@ func chacha20Poly1305Open(dst []byte, key []uint32, src, ad []byte) bool
func chacha20Poly1305Seal(dst []byte, key []uint32, src, ad []byte)
var (
useAVX2 = cpu.X86.HasAVX2 && cpu.X86.HasBMI2
useAVX2 = cpu.X86.HasSSSE3 && cpu.X86.HasAVX2 && cpu.X86.HasBMI2
)
// setupState writes a ChaCha20 input matrix to state. See
@@ -47,7 +47,7 @@ func setupState(state *[16]uint32, key *[32]byte, nonce []byte) {
}
func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []byte {
if !cpu.X86.HasSSSE3 {
if !useAVX2 {
return c.sealGeneric(dst, nonce, plaintext, additionalData)
}
@@ -66,7 +66,7 @@ func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []
}
func (c *chacha20poly1305) open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
if !cpu.X86.HasSSSE3 {
if !useAVX2 {
return c.openGeneric(dst, nonce, ciphertext, additionalData)
}
File diff suppressed because it is too large. Load diff
+7
View File
@@ -663,6 +663,13 @@ func (c *client) Add(key AddedKey) error {
constraints = append(constraints, agentConstrainConfirm)
}
for _, ext := range key.ConstraintExtensions {
constraints = append(constraints, ssh.Marshal(constrainExtensionAgentMsg{
ExtensionName: ext.ExtensionName,
ExtensionDetails: ext.ExtensionDetails,
})...)
}
cert := key.Certificate
if cert == nil {
return c.insertKey(key.PrivateKey, key.Comment, constraints)
+18 -5
View File
@@ -32,8 +32,10 @@ type keyring struct {
var errLocked = errors.New("agent: locked")
// NewKeyring returns an Agent that holds keys in memory. It is safe
// for concurrent use by multiple goroutines.
// NewKeyring returns an Agent that holds keys in memory. It is safe for
// concurrent use by multiple goroutines.
//
// The returned Agent only supports the "lifetime" constraint.
func NewKeyring() Agent {
return &keyring{}
}
@@ -143,15 +145,26 @@ func (r *keyring) List() ([]*Key, error) {
return ids, nil
}
// Insert adds a private key to the keyring. If a certificate
// is given, that certificate is added as public key. Note that
// any constraints given are ignored.
// Add adds a private key to the keyring. If a certificate is given, that
// certificate is added as public key.
//
// Add returns an error if key contains ConstraintExtensions or
// ConfirmBeforeUse.
func (r *keyring) Add(key AddedKey) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
if key.ConfirmBeforeUse {
return errors.New("agent: confirm before use constraint is not supported")
}
if len(key.ConstraintExtensions) > 0 {
return errors.New("agent: constraint extensions are present but not supported")
}
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
if err != nil {
+6
View File
@@ -270,6 +270,9 @@ func parseEd25519Key(req []byte) (*AddedKey, error) {
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
if len(k.Priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("agent: bad ED25519 key size: %d", len(k.Priv))
}
priv := ed25519.PrivateKey(k.Priv)
addedKey := &AddedKey{PrivateKey: &priv, Comment: k.Comments}
@@ -336,6 +339,9 @@ func parseEd25519Cert(req []byte) (*AddedKey, error) {
if err != nil {
return nil, err
}
if len(k.Priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("agent: bad ED25519 key size: %d", len(k.Priv))
}
priv := ed25519.PrivateKey(k.Priv)
cert, ok := pubKey.(*ssh.Certificate)
if !ok {
+17 -1
View File
@@ -348,6 +348,9 @@ func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey)
if cert.CertType != HostCert {
return fmt.Errorf("ssh: certificate presented as a host key has type %d", cert.CertType)
}
if c.IsHostAuthority == nil {
return errors.New("ssh: cannot verify certificate, IsHostAuthority not set")
}
if !c.IsHostAuthority(cert.SignatureKey, addr) {
return fmt.Errorf("ssh: no authorities for hostname: %v", addr)
}
@@ -375,6 +378,9 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
if cert.CertType != UserCert {
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
}
if c.IsUserAuthority == nil {
return nil, errors.New("ssh: cannot verify certificate, IsUserAuthority not set")
}
if !c.IsUserAuthority(cert.SignatureKey) {
return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority")
}
@@ -438,7 +444,17 @@ func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(CertTimeInfinity) && (unixNow >= before || before < 0) {
return fmt.Errorf("ssh: cert has expired")
}
if err := cert.SignatureKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
// Match OpenSSH: the SK user-presence flag is never enforced on a
// certificate's CA signature. OpenSSH calls sshkey_verify with
// detailsp==NULL in sshkey.c:cert_parse, so the UP/UV flags are
// not even extracted. The UP bit on a CA signature reflects the
// CA operator's presence at signing time, which has no bearing on
// whether the user being authenticated is present now; enforcing
// it here would only break interop with certificates issued by
// non-interactive SK CAs. skKeyWithoutUP is a no-op for non-SK
// keys (the common case).
caKey := skKeyWithoutUP(cert.SignatureKey)
if err := caKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
return fmt.Errorf("ssh: certificate signature does not verify")
}
+59 -6
View File
@@ -11,6 +11,7 @@ import (
"io"
"log"
"sync"
"sync/atomic"
)
const (
@@ -131,11 +132,17 @@ func (r RejectionReason) String() string {
return fmt.Sprintf("unknown reason %d", int(r))
}
func min(a uint32, b int) uint32 {
if a < uint32(b) {
return a
// minPayloadSize returns min(limit, length) clamped to a uint32. It is used
// to compute the size of the next channel data packet from the remaining
// payload. The comparison is done in int64 because length is an int — on
// 64-bit systems len(data) can exceed 2^32, and a direct uint32(length)
// cast would silently truncate to 0 at every multiple of 2^32, causing
// WriteExtended's loop to spin without making progress.
func minPayloadSize(limit uint32, length int) uint32 {
if int64(length) > int64(limit) {
return limit
}
return uint32(b)
return uint32(length)
}
type channelDirection uint8
@@ -177,6 +184,12 @@ type channel struct {
// with WantReply=true outstanding. This lock is held by a
// goroutine that has such an outgoing request pending.
sentRequestMu sync.Mutex
// sentRequestPending is set to true while a SendRequest call with
// WantReply=true is in flight. handlePacket uses it as a gate: responses
// arriving while no request is pending are dropped to prevent a
// misbehaving peer from stalling the mux read loop by filling ch.msg
// with unsolicited channelRequestSuccess/Failure messages.
sentRequestPending atomic.Bool
incomingRequests chan *Request
@@ -251,7 +264,7 @@ func (ch *channel) WriteExtended(data []byte, extendedCode uint32) (n int, err e
ch.writeMu.Unlock()
for len(data) > 0 {
space := min(ch.maxRemotePayload, len(data))
space := minPayloadSize(ch.maxRemotePayload, len(data))
if space, err = ch.remoteWin.reserve(space); err != nil {
return n, err
}
@@ -460,6 +473,18 @@ func (ch *channel) handlePacket(packet []byte) error {
}
ch.incomingRequests <- &req
case *channelRequestSuccessMsg, *channelRequestFailureMsg:
// Drop responses that arrive when no SendRequest is waiting, to
// prevent a malicious peer from filling ch.msg and stalling the
// mux read loop. The non-blocking send additionally protects the
// loop if a well-behaved caller is slow to read.
if !ch.sentRequestPending.Load() {
return nil
}
select {
case ch.msg <- msg:
default:
}
default:
ch.msg <- msg
}
@@ -530,7 +555,17 @@ func (ch *channel) Reject(reason RejectionReason, message string) error {
Language: "en",
}
ch.decided = true
return ch.sendMessage(reject)
err := ch.sendMessage(reject)
// Remove the channel from the mux to prevent memory leaks.
// Do not call ch.close() here: no goroutine holds a reference to a
// rejected channel's internal channels (msg, incomingRequests), so
// removing it from chanList is sufficient for GC. Calling close()
// would race with the mux loop goroutine (handlePacket or dropAll),
// causing a panic from closing an already-closed channel.
ch.mux.chanList.remove(ch.localId)
return err
}
func (ch *channel) Read(data []byte) (int, error) {
@@ -586,6 +621,24 @@ func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo
if wantReply {
ch.sentRequestMu.Lock()
defer ch.sentRequestMu.Unlock()
// Open the gate so that responses arriving while this request is in
// flight are allowed to reach ch.msg. Responses arriving while no
// request is pending are dropped by handlePacket.
ch.sentRequestPending.Store(true)
defer ch.sentRequestPending.Store(false)
// Drain any spurious responses that may have been buffered. This
// prevents a previously buffered unexpected response from being
// consumed instead of the actual response for this request.
drain:
for {
select {
case <-ch.msg:
default:
break drain
}
}
}
msg := channelRequestMsg{
+1 -1
View File
@@ -407,7 +407,7 @@ func (c *gcmCipher) readCipherPacket(seqNum uint32, r io.Reader) ([]byte, error)
return nil, fmt.Errorf("ssh: illegal padding %d", padding)
}
if int(padding+1) >= len(plain) {
if int(padding)+1 >= len(plain) {
return nil, fmt.Errorf("ssh: padding %d too large", padding)
}
plain = plain[1 : length-uint32(padding)]
+58
View File
@@ -469,6 +469,12 @@ func parseRSA(in []byte) (out PublicKey, rest []byte, err error) {
return nil, nil, err
}
// 8192 bits is also the maximum RSA key size accepted by crypto/tls for
// signature verification:
// https://github.com/golang/go/blob/69801b25/src/crypto/tls/handshake_client.go#L1096
if w.N.BitLen() > 8192 {
return nil, nil, errors.New("ssh: rsa modulus too large")
}
if w.E.BitLen() > 24 {
return nil, nil, errors.New("ssh: exponent too large")
}
@@ -574,6 +580,24 @@ func checkDSAParams(param *dsa.Parameters) error {
return fmt.Errorf("ssh: unsupported DSA key size %d", l)
}
// FIPS 186-2 specifies that Q must be exactly 160 bits. We must enforce
// this to prevent DoS attacks where an attacker sends a huge Q which makes
// verification slow.
if l := param.Q.BitLen(); l != 160 {
return fmt.Errorf("ssh: unsupported DSA sub-prime size %d", l)
}
// The generator G is an element of the group, so it must be strictly less
// than the modulus P.
if param.G.Cmp(param.P) >= 0 {
return errors.New("ssh: DSA generator larger than modulus")
}
// G must be positive.
if param.G.Sign() <= 0 {
return errors.New("ssh: DSA generator must be positive")
}
return nil
}
@@ -596,6 +620,14 @@ func parseDSA(in []byte) (out PublicKey, rest []byte, err error) {
return nil, nil, err
}
// The public value Y must be a non-zero element of the group, i.e.
// strictly between 0 and P. crypto/dsa.Verify does not range-check Y,
// so we reject out-of-range values here to prevent a maliciously
// oversized Y from slowing verification.
if w.Y.Sign() <= 0 || w.Y.Cmp(w.P) >= 0 {
return nil, nil, errors.New("ssh: DSA public value Y out of range")
}
key := &dsaPublicKey{
Parameters: param,
Y: w.Y,
@@ -869,11 +901,25 @@ type skFields struct {
Counter uint32
}
// flagUserPresence is the "user present" bit (UP) in the SK signature
// flags, matching the FIDO CTAP2 authenticatorData UP flag. See
// openssh/PROTOCOL.u2f.
const flagUserPresence = 0x01
// errSKMissingUserPresence is returned by SK key Verify methods when
// the signature does not assert user presence and the key was not
// marked as no-touch-required.
var errSKMissingUserPresence = errors.New("ssh: signature missing required user presence flag")
type skECDSAPublicKey struct {
// application is a URL-like string, typically "ssh:" for SSH.
// see openssh/PROTOCOL.u2f for details.
application string
ecdsa.PublicKey
// noTouchRequired, when true, disables the default user-presence
// check in Verify. It is set by skKeyWithoutUP on a clone of the
// key, never on an instance shared across authentication attempts.
noTouchRequired bool
}
func (k *skECDSAPublicKey) Type() string {
@@ -959,6 +1005,10 @@ func (k *skECDSAPublicKey) Verify(data []byte, sig *Signature) error {
return err
}
if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired {
return errSKMissingUserPresence
}
blob := struct {
ApplicationDigest []byte `ssh:"rest"`
Flags byte
@@ -992,6 +1042,10 @@ type skEd25519PublicKey struct {
// see openssh/PROTOCOL.u2f for details.
application string
ed25519.PublicKey
// noTouchRequired, when true, disables the default user-presence
// check in Verify. It is set by skKeyWithoutUP on a clone of the
// key, never on an instance shared across authentication attempts.
noTouchRequired bool
}
func (k *skEd25519PublicKey) Type() string {
@@ -1066,6 +1120,10 @@ func (k *skEd25519PublicKey) Verify(data []byte, sig *Signature) error {
return err
}
if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired {
return errSKMissingUserPresence
}
blob := struct {
ApplicationDigest []byte `ssh:"rest"`
Flags byte
+7 -2
View File
@@ -160,8 +160,13 @@ func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool
// IsRevoked can be used as a callback in ssh.CertChecker
func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool {
_, ok := db.revoked[string(key.Marshal())]
return ok
if _, ok := db.revoked[string(key.Marshal())]; ok {
return true
}
if _, ok := db.revoked[string(key.SignatureKey.Marshal())]; ok {
return true
}
return false
}
const markerCert = "@cert-authority"
+32 -4
View File
@@ -91,9 +91,10 @@ type mux struct {
incomingChannels chan NewChannel
globalSentMu sync.Mutex
globalResponses chan interface{}
incomingRequests chan *Request
globalSentMu sync.Mutex
globalSentPending atomic.Bool
globalResponses chan interface{}
incomingRequests chan *Request
errCond *sync.Cond
err error
@@ -141,6 +142,24 @@ func (m *mux) SendRequest(name string, wantReply bool, payload []byte) (bool, []
if wantReply {
m.globalSentMu.Lock()
defer m.globalSentMu.Unlock()
// Open the gate so that responses arriving while this request is in
// flight are allowed to reach globalResponses. Any response arriving
// while no request is pending is dropped by handleGlobalPacket.
m.globalSentPending.Store(true)
defer m.globalSentPending.Store(false)
// Drain any spurious responses that may have been buffered. This prevents
// a previously buffered unexpected response from being consumed instead
// of the actual response for this request.
drain:
for {
select {
case <-m.globalResponses:
default:
break drain
}
}
}
if err := m.sendMessage(globalRequestMsg{
@@ -267,7 +286,16 @@ func (m *mux) handleGlobalPacket(packet []byte) error {
mux: m,
}
case *globalRequestSuccessMsg, *globalRequestFailureMsg:
m.globalResponses <- msg
// Drop responses that arrive when no SendRequest is waiting, to
// prevent a malicious peer from staging responses for a future
// caller.
if !m.globalSentPending.Load() {
return nil
}
select {
case m.globalResponses <- msg:
default:
}
default:
panic(fmt.Sprintf("not a global message %#v", msg))
}
+114 -13
View File
@@ -34,15 +34,20 @@ type Permissions struct {
// or not supported.
CriticalOptions map[string]string
// Extensions are extra functionality that the server may
// offer on authenticated connections. Lack of support for an
// extension does not preclude authenticating a user. Common
// extensions are "permit-agent-forwarding",
// "permit-X11-forwarding". The Go SSH library currently does
// not act on any extension, and it is up to server
// implementations to honor them. Extensions can be used to
// pass data from the authentication callbacks to the server
// application layer.
// Extensions are extra functionality that the server may offer on
// authenticated connections. Lack of support for an extension does not
// preclude authenticating a user. Common extensions are
// "permit-agent-forwarding", "permit-X11-forwarding". In general the Go
// SSH library does not act on extensions and it is up to server
// implementations to honor them; extensions can also be used to pass data
// from the authentication callbacks to the server application layer.
//
// The one extension acted upon by this library is "no-touch-required",
// which applies only to security-key public keys
// (sk-ecdsa-sha2-nistp256@openssh.com and sk-ssh-ed25519@openssh.com).
// When present, it waives the default requirement that SK signatures
// assert user presence (i.e. a physical touch of the authenticator)
// during signature verification.
Extensions map[string]string
// ExtraData allows to store user defined data.
@@ -84,6 +89,79 @@ type ServerPreAuthConn interface {
SendAuthBanner(string) error
}
// noTouchRequiredExtension is the extension name used by OpenSSH in
// authorized_keys options and certificate extensions to mark keys
// whose signatures do not need to assert user presence (touch). See
// ssh-keygen(1) and sshd(8).
const noTouchRequiredExtension = "no-touch-required"
// noTouchAllowed reports whether the user presence requirement on
// SK signatures should be waived for this authentication attempt. The
// requirement is waived when the "no-touch-required" extension is
// present either in the Permissions returned by the auth callback
// (authorized_keys-level opt-out) or in the certificate's own
// Extensions (CA-level opt-out), matching OpenSSH behavior. OpenSSH
// reads the per-key opt-out only from cert Extensions and
// authorized_keys options (never from CriticalOptions); we follow the
// same rule.
func noTouchAllowed(pubKey PublicKey, perms *Permissions) bool {
if perms != nil {
if _, ok := perms.Extensions[noTouchRequiredExtension]; ok {
return true
}
}
if cert, ok := pubKey.(*Certificate); ok {
if _, ok := cert.Extensions[noTouchRequiredExtension]; ok {
return true
}
}
return false
}
// skKeyWithoutUP returns a PublicKey equivalent to pubKey but whose
// Verify accepts SK signatures with the user-presence flag clear. If
// pubKey is not (and does not wrap) an SK key, pubKey is returned
// unchanged. The returned value never mutates pubKey: for SK keys a
// shallow copy is made so that the noTouchRequired flag is set only on
// the clone.
//
// The implementation is iterative rather than recursive. When pubKey
// is a *Certificate we unwrap exactly one level to look at the inner
// key. The SSH cert format forbids Certificate.Key from being another
// Certificate (parseCert rejects it), but nothing stops callers from
// constructing such a value directly in Go; a recursive descent could
// otherwise be driven to unbounded depth by a hand-crafted or cyclic
// Certificate. A malformed input of that shape simply returns
// unchanged here.
func skKeyWithoutUP(pubKey PublicKey) PublicKey {
cert, isCert := pubKey.(*Certificate)
target := pubKey
if isCert {
target = cert.Key
}
var cloned PublicKey
switch k := target.(type) {
case *skECDSAPublicKey:
c := *k
c.noTouchRequired = true
cloned = &c
case *skEd25519PublicKey:
c := *k
c.noTouchRequired = true
cloned = &c
default:
// Not an SK key (or a pathological *Certificate wrapping
// another *Certificate): pubKey is already usable for Verify.
return pubKey
}
if !isCert {
return cloned
}
c := *cert
c.Key = cloned
return &c
}
// ServerConfig holds server specific configuration data.
type ServerConfig struct {
// Config contains configuration shared between client and server.
@@ -242,8 +320,10 @@ func (c *pubKeyCache) add(candidate cachedPubKey) {
type ServerConn struct {
Conn
// If the succeeding authentication callback returned a
// non-nil Permissions pointer, it is stored here.
// If the succeeding authentication callback returned a non-nil Permissions
// pointer, it is stored here. These are the permissions from the final,
// successful authentication method. Permissions returned by callbacks that
// return PartialSuccessError are not preserved and must be nil.
Permissions *Permissions
}
@@ -737,8 +817,15 @@ userAuthLoop:
}
signedData := buildDataSignedForAuth(sessionID, userAuthReq, algo, pubKeyData)
if err := pubKey.Verify(signedData, sig); err != nil {
// pubKey is reused below for VerifiedPublicKeyCallback and
// must remain the key as presented by the client; derive a
// separate value for Verify that carries any applicable
// no-touch-required opt-out.
pubKeyForVerify := pubKey
if noTouchAllowed(pubKey, candidate.perms) {
pubKeyForVerify = skKeyWithoutUP(pubKey)
}
if err := pubKeyForVerify.Verify(signedData, sig); err != nil {
return nil, err
}
@@ -750,6 +837,13 @@ userAuthLoop:
// considered verified and the callback must not run.
perms, authErr = config.VerifiedPublicKeyCallback(s, pubKey, perms, algo)
}
if authErr == nil && perms != nil && perms.CriticalOptions != nil {
if saco := perms.CriticalOptions[sourceAddressCriticalOption]; saco != "" {
if err := checkSourceAddress(s.RemoteAddr(), saco); err != nil {
authErr = err
}
}
}
}
case "gssapi-with-mic":
if authConfig.GSSAPIWithMICConfig == nil {
@@ -824,6 +918,13 @@ userAuthLoop:
var failureMsg userAuthFailureMsg
if partialSuccess, ok := authErr.(*PartialSuccessError); ok {
// Permissions are not preserved between authentication steps. To
// avoid confusion about the final state of the connection, we
// disallow returning non-nil Permissions combined with
// PartialSuccessError.
if perms != nil {
return nil, errors.New("ssh: permissions must be nil when returning PartialSuccessError")
}
// After a partial success error we don't allow changing the user
// name and execute the NoClientAuthCallback.
partialSuccessReturned = true
+6 -6
View File
@@ -845,7 +845,7 @@ github.com/justinas/alice
# github.com/kevinburke/ssh_config v1.2.0
## explicit
github.com/kevinburke/ssh_config
# github.com/klauspost/compress v1.18.5
# github.com/klauspost/compress v1.18.6
## explicit; go 1.24
github.com/klauspost/compress
github.com/klauspost/compress/flate
@@ -1147,10 +1147,10 @@ github.com/mschoch/smat
# github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
## explicit
github.com/munnerz/goautoneg
# github.com/nats-io/jwt/v2 v2.8.1
# github.com/nats-io/jwt/v2 v2.8.2
## explicit; go 1.25.0
github.com/nats-io/jwt/v2
# github.com/nats-io/nats-server/v2 v2.14.0
# github.com/nats-io/nats-server/v2 v2.14.2
## explicit; go 1.25.0
github.com/nats-io/nats-server/v2/conf
github.com/nats-io/nats-server/v2/internal/fastrand
@@ -1176,8 +1176,8 @@ github.com/nats-io/nats.go/internal/parser
github.com/nats-io/nats.go/internal/syncx
github.com/nats-io/nats.go/jetstream
github.com/nats-io/nats.go/util
# github.com/nats-io/nkeys v0.4.15
## explicit; go 1.24.0
# github.com/nats-io/nkeys v0.4.16
## explicit; go 1.25.0
github.com/nats-io/nkeys
# github.com/nats-io/nuid v1.0.1
## explicit
@@ -2412,7 +2412,7 @@ go.yaml.in/yaml/v2
# go.yaml.in/yaml/v3 v3.0.4
## explicit; go 1.16
go.yaml.in/yaml/v3
# golang.org/x/crypto v0.51.0
# golang.org/x/crypto v0.52.0
## explicit; go 1.25.0
golang.org/x/crypto/argon2
golang.org/x/crypto/bcrypt