mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-17 08:10:38 -04:00
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:
1 parent
4414bd0d08
commit
5f88cc686c
58 files changed
+3201
-6425
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -0,0 +1,14 @@
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
languages = {
|
||||
go.enable = true;
|
||||
};
|
||||
|
||||
packages = with pkgs; [
|
||||
golangci-lint
|
||||
];
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
|
||||
inputs:
|
||||
nixpkgs:
|
||||
url: github:cachix/devenv-nixpkgs/rolling
|
||||
@@ -1,2 +1,3 @@
|
||||
* -text
|
||||
*.bin -text -diff
|
||||
*.md text eol=lf
|
||||
+700
-700
File diff suppressed because it is too large.
Load diff
+78
-78
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+325
-4857
File diff suppressed because it is too large.
Load diff
+7
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Vendored
+6
-6
@@ -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
|
||||
|
||||
Reference in new issue
Block a user