Compare commits
29 commits
feature/ad
...
master
Author | SHA1 | Date | |
---|---|---|---|
20586d8ada | |||
f099edb17b | |||
764b669295 | |||
c88eea1f82 | |||
5f451c8818 | |||
42f4507638 | |||
b390778201 | |||
30af614558 | |||
45e73a6f8e | |||
87fe8db674 | |||
5e86f53b0e | |||
9eb742da77 | |||
98cfd82313 | |||
f93e33b49b | |||
6ae96c1d77 | |||
809bd90352 | |||
32a7e64538 | |||
d6fe034453 | |||
9364d60b96 | |||
6988fcedae | |||
8835b23ed3 | |||
bd8eb7cc60 | |||
2e56c13946 | |||
195854a45b | |||
43e300c773 | |||
568bdc67e8 | |||
8637515869 | |||
db9b93b2e6 | |||
543247e4d9 |
170 changed files with 7890 additions and 5259 deletions
35
.forgejo/workflows/publish.yml
Normal file
35
.forgejo/workflows/publish.yml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
image:
|
||||||
|
name: Publish NuGet packages
|
||||||
|
runs-on: docker
|
||||||
|
container: git.frostfs.info/truecloudlab/env:dotnet-8.0
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Build NuGet packages
|
||||||
|
# `dotnet build` implies and replaces `dotnet pack` thanks to `GeneratePackageOnBuild`
|
||||||
|
run: dotnet build
|
||||||
|
|
||||||
|
- name: Publish unsigned NuGet packages
|
||||||
|
run: |-
|
||||||
|
dotnet nuget add source \
|
||||||
|
--name "$NUGET_REGISTRY" \
|
||||||
|
--username "$NUGET_REGISTRY_USER" \
|
||||||
|
--password "$NUGET_REGISTRY_PASSWORD" \
|
||||||
|
--store-password-in-clear-text \
|
||||||
|
"$NUGET_REGISTRY_URL"
|
||||||
|
find -iname '*.nupkg' | grep . | xargs -d'\n' -t -n1 \
|
||||||
|
dotnet nuget push --source "$NUGET_REGISTRY"
|
||||||
|
env:
|
||||||
|
NUGET_REGISTRY: TrueCloudLab
|
||||||
|
NUGET_REGISTRY_URL: https://git.frostfs.info/api/packages/TrueCloudLab/nuget/index.json
|
||||||
|
NUGET_REGISTRY_USER: ${{secrets.NUGET_REGISTRY_USER}}
|
||||||
|
NUGET_REGISTRY_PASSWORD: ${{secrets.NUGET_REGISTRY_PASSWORD}}
|
||||||
|
if: >-
|
||||||
|
startsWith(github.ref, 'refs/tags/v') &&
|
||||||
|
(github.event_name == 'workflow_dispatch' || github.event_name == 'push')
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -32,5 +32,8 @@ antlr-*.jar
|
||||||
|
|
||||||
# binary
|
# binary
|
||||||
bin/
|
bin/
|
||||||
release/
|
|
||||||
obj/
|
obj/
|
||||||
|
|
||||||
|
# Repository signing keys
|
||||||
|
release/maintainer.*
|
||||||
|
release/ca.*
|
||||||
|
|
58
Makefile
Normal file
58
Makefile
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
DOTNET?=dotnet
|
||||||
|
DOCKER?=docker
|
||||||
|
|
||||||
|
NUGET_REGISTRY?=TrueCloudLab
|
||||||
|
NUGET_REGISTRY_URL?=https://git.frostfs.info/api/packages/TrueCloudLab/nuget/index.json
|
||||||
|
NUGET_REGISTRY_USER?=
|
||||||
|
NUGET_REGISTRY_PASSWORD?=
|
||||||
|
|
||||||
|
NUPKG=find -iname '*.nupkg' | grep . | xargs -d'\n' -t -r -n1
|
||||||
|
RFC3161_TSA?=http://timestamp.digicert.com
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
$(DOTNET) build
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: sign
|
||||||
|
sign: export NUGET_CERT_REVOCATION_MODE=offline
|
||||||
|
sign: release/maintainer.pfx
|
||||||
|
$(NUPKG) $(DOTNET) nuget sign --overwrite --certificate-path $< --timestamper "$(RFC3161_TSA)"
|
||||||
|
@rm -v "$<" # maintainer.pfx is not password protected and must be ephemeral
|
||||||
|
$(NUPKG) $(DOTNET) nuget verify
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: publish
|
||||||
|
publish:
|
||||||
|
$(NUPKG) $(DOTNET) nuget verify
|
||||||
|
$(NUPKG) $(DOTNET) nuget push --source "$(NUGET_REGISTRY)"
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: nuget-registry
|
||||||
|
nuget-registry:
|
||||||
|
ifeq (,$(NUGET_REGISTRY_USER))
|
||||||
|
$(error NUGET_REGISTRY_USER not set)
|
||||||
|
endif
|
||||||
|
ifeq (,$(NUGET_REGISTRY_PASSWORD))
|
||||||
|
$(error NUGET_REGISTRY_PASSWORD not set)
|
||||||
|
endif
|
||||||
|
$(DOTNET) nuget add source \
|
||||||
|
--name "$(NUGET_REGISTRY)" \
|
||||||
|
--username "$(NUGET_REGISTRY_USER)" \
|
||||||
|
--password "$(NUGET_REGISTRY_PASSWORD)" \
|
||||||
|
--store-password-in-clear-text \
|
||||||
|
"$(NUGET_REGISTRY_URL)"
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
-$(NUPKG) rm -v
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: container
|
||||||
|
container:
|
||||||
|
$(DOCKER) run --pull=always --rm -it -v "$$PWD:/src" -w /src git.frostfs.info/truecloudlab/env:dotnet-8.0
|
||||||
|
|
||||||
|
|
||||||
|
include release/codesign.mk
|
|
@ -47,9 +47,9 @@ var placementPolicy = new FrostFsPlacementPolicy(true, new FrostFsReplica(1));
|
||||||
var createContainerParam = new PrmContainerCreate(
|
var createContainerParam = new PrmContainerCreate(
|
||||||
new FrostFsContainerInfo(BasicAcl.PublicRW, new FrostFsPlacementPolicy(true, new FrostFsReplica(1))));
|
new FrostFsContainerInfo(BasicAcl.PublicRW, new FrostFsPlacementPolicy(true, new FrostFsReplica(1))));
|
||||||
|
|
||||||
var containerId = await client.CreateContainerAsync(createContainerParam);
|
var containerId = await client.PutContainerAsync(createContainerParam);
|
||||||
|
|
||||||
using var fileStream = File.OpenRead(@"C:\Users\Paul\Pictures\cat.jpeg");
|
using var fileStream = File.OpenRead(@"cat.jpeg");
|
||||||
|
|
||||||
var param = new PrmObjectPut
|
var param = new PrmObjectPut
|
||||||
{
|
{
|
||||||
|
|
BIN
keyfile.snk
Normal file
BIN
keyfile.snk
Normal file
Binary file not shown.
82
release/README.md
Normal file
82
release/README.md
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# Release process
|
||||||
|
|
||||||
|
## Preparing release
|
||||||
|
|
||||||
|
_TBD_
|
||||||
|
|
||||||
|
## Trusting TrueCloudLab code signing CA certificate
|
||||||
|
|
||||||
|
Verifying signatures (and signing) TrueCloudLab packages requires adding
|
||||||
|
[TrueCloudLab Code Signing CA](ca.cert) to the list of trusted roots.
|
||||||
|
|
||||||
|
On Linux this can be done by appending [release/ca.cert](ca.cert) to one of:
|
||||||
|
|
||||||
|
- `/etc/pki/ca-trust/extracted/pem/objsign-ca-bundle.pem`: compatible with
|
||||||
|
[update-ca-trust] and originally proposed in [.NET design docs]
|
||||||
|
- `…/dotnet/sdk/X.Y.ZZZ/trustedroots/codesignctl.pem`: [fallback] codesigning certificate trust list for .NET
|
||||||
|
|
||||||
|
[update-ca-trust]: https://www.linux.org/docs/man8/update-ca-trust.html
|
||||||
|
[.NET design docs]: https://github.com/dotnet/designs/blob/main/accepted/2021/signed-package-verification/re-enable-signed-package-verification-technical.md#linux
|
||||||
|
[fallback]: https://github.com/dotnet/sdk/blob/11150c0ec9020625308edeec555a8b78dbfb2aa5/src/Layout/redist/trustedroots/README.md
|
||||||
|
|
||||||
|
## Signing Nuget packages
|
||||||
|
|
||||||
|
Repository maintainer places `maintainer.cert` and `maintainer.key` (see below
|
||||||
|
regarding obtaining these files) into `release/` directory and then
|
||||||
|
executes:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make build sign
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uploading packages to Nuget registry
|
||||||
|
|
||||||
|
**IMPORTANT: the following steps upload all `*.nupkg` files located under
|
||||||
|
`src/`. Maintainer MUST make sure that no unnecessary package versions will be
|
||||||
|
uploaded to the registry.**
|
||||||
|
|
||||||
|
Configure registry credentials (once per machine):
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make nuget-registry NUGET_REGISTRY_USER=username NUGET_REGISTRY_PASSWORD=token
|
||||||
|
```
|
||||||
|
|
||||||
|
Publish all locally built packages (implicitly clear existing `*.nupkg` and
|
||||||
|
rebuild current version only):
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make clean build sign publish
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Obtaining release signing certificate
|
||||||
|
|
||||||
|
Repository maintainer owns and keeps safe the release signing key
|
||||||
|
(`maintainer.key`). Private key should never leave maintainer's machine and
|
||||||
|
should be considered a highly sensitive secret.
|
||||||
|
|
||||||
|
- Generating new maintainer key and the corresponding CSR:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make maintainer.csr
|
||||||
|
...lines skipped...
|
||||||
|
Enter PEM pass phrase:
|
||||||
|
Verifying - Enter PEM pass phrase:
|
||||||
|
-----
|
||||||
|
IMPORTANT: Keep maintainer.key private!
|
||||||
|
|
||||||
|
Certificate signing request is ready.
|
||||||
|
Send maintainer.csr to CA administrator to obtain the certificate.
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting CSR (`maintainer.csr`) does not contain any sensitive
|
||||||
|
cryptographic material and may be passed to CA administrator through regular
|
||||||
|
communication channels.
|
||||||
|
|
||||||
|
- CA administrator then issues the certificate (`make maintainer.cert`) and
|
||||||
|
sends it back to the maintainer to be used in combination with
|
||||||
|
`maintainer.key`
|
||||||
|
|
||||||
|
This procedure should be repeated once per machine per `maintainer.cert`
|
||||||
|
lifetime (1 year) - typically just once per year since we expect the
|
||||||
|
maintainer to use only a single computer to sign releases.
|
34
release/ca.cert
Normal file
34
release/ca.cert
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIF2zCCA8OgAwIBAgIUa9xC/RgvFtUG/xeR016nn0B4K0YwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwdTELMAkGA1UEBhMCUlUxFTATBgNVBAoMDFRydWVDbG91ZExhYjEVMBMGA1UE
|
||||||
|
CwwMVHJ1ZUNsb3VkTGFiMTgwNgYDVQQDDC9UcnVlQ2xvdWRMYWIgQ29kZSBTaWdu
|
||||||
|
aW5nIENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0yNTA0MTAxNTI2MTFaFw0zNTA0
|
||||||
|
MDgxNTI2MTFaMHUxCzAJBgNVBAYTAlJVMRUwEwYDVQQKDAxUcnVlQ2xvdWRMYWIx
|
||||||
|
FTATBgNVBAsMDFRydWVDbG91ZExhYjE4MDYGA1UEAwwvVHJ1ZUNsb3VkTGFiIENv
|
||||||
|
ZGUgU2lnbmluZyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4ICDwAwggIKAoICAQCyANB4cjf+ZEAFx9RiUYXCAOPMV+jyqgcVbhzh2YKc
|
||||||
|
9SlvGKRlc00Ar1RlFcrycdkIrTKmhhobiWsFp7UgphwLqRTCb5NB6qfUoWhnfiD9
|
||||||
|
m0OBgeVX5wivVaibRI9PSTbFDcIhYUiNvwFJ6GduH/9zOxf1BvuL7LMaoyhIDcg/
|
||||||
|
XVLuekE2lnX83zsedv0v/2jyyMY9Ct6N2BXzyHSAzSdYYg0F9Qu9fIMAPjoKhWPc
|
||||||
|
PnotqaACjb1DScLUr3E/o2W1FfprTT2Pip/0AXxO4wixl4QWh9HeOKV22KRcCHo6
|
||||||
|
3wNdg5q1ZVGTNBW0+yoB4jsSG8/JM+2Ujhc1ZnYH10armvGq/0Oc2YQE00960Wy8
|
||||||
|
t0drCFWJUO1XHNeBxkkupmj7N1TAPbixtfiGZJhECOWOJwyMpcKixlt5P0cNH4N/
|
||||||
|
p3vjyrGQxGLBIkgV/QgjfGkpTHKT1/H40YK6DliWJc01KfNTqn0K+/TIyF0n26kD
|
||||||
|
BWYVlvDh5P1+V9DGuD2zeXB3PstoifD/Pd7D8wuqpm17noFE19MLp94xv03q9nEa
|
||||||
|
jRMEd2J2buTLvMh5BBVH0Sm38QAHpSIZ9O3dSLvvjlALbVtwmcsNE9fgxiue3vTB
|
||||||
|
iXNW8wWs+/DMYwbWyBoCwORxVOdOyc1JLn7qAAEUBweilPVXpWuzMLdUsifPiqrV
|
||||||
|
dQIDAQABo2MwYTAdBgNVHQ4EFgQUEz4y/RvQMmbUFvf5JbGe/0PZR90wHwYDVR0j
|
||||||
|
BBgwFoAUEz4y/RvQMmbUFvf5JbGe/0PZR90wDwYDVR0TAQH/BAUwAwEB/zAOBgNV
|
||||||
|
HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADggIBAF79W9hMGnrKUWLDOcoeXDEn
|
||||||
|
+gZxd5rjeF0tNEQGbWKGTJAGQdprOkzWQ47PewpFeS+ePpjOglBAt7cV2ZnOugAT
|
||||||
|
Brx31vYpNAvYnUCYn+/IUqD8S/U4uErVS9zG9QjirZWo56eJP62vnScKuApCQCbA
|
||||||
|
pp0zrIyJ+2lQKzlMOENRqzYYA/UTOqYTtnW6x2D8goVqCmDXpgpxEp5XliFjJSr6
|
||||||
|
dOjiopNWMvaV3R/Bnd4i41taM7M6HpIV+gbXmEKEFS0ejVfzT8z1pTigN7GBqbxf
|
||||||
|
nXD03eLUIsbMDv4ZQPrheN7nKnjRUn8kxz0SSK1m2YDrXW51m8fOs6aTvwC/cNe+
|
||||||
|
FJMqQMF32i4IXVfUbyUJi+JMvawqm2wEY46vrh7siprY6rXsAzCKJo07i6jvUwnT
|
||||||
|
TXMldSCPgTEqzT2JBzzr0tRfuPKsv0/NqflHvwfuMRCpcZ7jJZ700iN92xXkiQHP
|
||||||
|
DmCZOILXcNclAth3nAnyY4XE5a8myv8bwYaPdJdIFlV+BoU/8mClDeA8ck4rDy12
|
||||||
|
T5YChKew2oiL4j4B6v9/yrDjD1IT0gv4BWyPhb/n390BCEXt8g9auNcT0s6O8kEc
|
||||||
|
VUDVc1519ocMCuWVuqUK9o2w0zu50/pBn4hVLfT3QyW8sqtlRKghOWtqZzigvCWF
|
||||||
|
VjATeO5F/Z7OSDebHUGv
|
||||||
|
-----END CERTIFICATE-----
|
74
release/codesign.mk
Normal file
74
release/codesign.mk
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
PKI_ROLE?=maintainer
|
||||||
|
PKI_DIR?=release
|
||||||
|
|
||||||
|
# Note: Only RSA signatures are supported (NU3013)
|
||||||
|
# https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu3013)
|
||||||
|
|
||||||
|
|
||||||
|
ifeq ($(PKI_ROLE),maintainer)
|
||||||
|
.PHONY: maintainer.csr
|
||||||
|
maintainer.csr: $(PKI_DIR)/maintainer.csr
|
||||||
|
$(PKI_DIR)/maintainer.csr: KEY=$(patsubst %.csr,%.key,$@)
|
||||||
|
$(PKI_DIR)/maintainer.csr:
|
||||||
|
openssl req \
|
||||||
|
-new \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-keyout $(KEY) \
|
||||||
|
-out $@ \
|
||||||
|
-sha256 \
|
||||||
|
-addext keyUsage=critical,digitalSignature \
|
||||||
|
-addext extendedKeyUsage=critical,codeSigning,msCodeCom \
|
||||||
|
-subj "/C=RU/O=TrueCloudLab/OU=TrueCloudLab/CN=frostfs-sdk-csharp Release Team"
|
||||||
|
@echo "IMPORTANT: Keep $(KEY) private!\n"
|
||||||
|
@echo "Certificate signing request is ready.\nSend $@ to CA administrator to obtain the certificate."
|
||||||
|
|
||||||
|
$(PKI_DIR)/maintainer.pfx: $(PKI_DIR)/maintainer.cert $(PKI_DIR)/maintainer.key $(PKI_DIR)/ca.cert
|
||||||
|
openssl verify \
|
||||||
|
-CAfile $(PKI_DIR)/ca.cert \
|
||||||
|
$(PKI_DIR)/maintainer.cert
|
||||||
|
openssl pkcs12 \
|
||||||
|
-export \
|
||||||
|
-out $@ \
|
||||||
|
-inkey $(PKI_DIR)/maintainer.key \
|
||||||
|
-in $(PKI_DIR)/maintainer.cert \
|
||||||
|
-CAfile $(PKI_DIR)/ca.cert \
|
||||||
|
-chain \
|
||||||
|
-passout pass:
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
ifeq ($(PKI_ROLE),ca)
|
||||||
|
.PHONY: maintainer.cert
|
||||||
|
maintainer.cert: $(PKI_DIR)/maintainer.cert
|
||||||
|
$(PKI_DIR)/maintainer.cert: CSR=$(patsubst %.cert,%.csr,$@)
|
||||||
|
$(PKI_DIR)/maintainer.cert: $(PKI_DIR)/ca.key $(PKI_DIR)/ca.cert
|
||||||
|
openssl req -noout -text -in $(CSR)
|
||||||
|
@read -p "Review the CSR above. Press Enter to continue, Ctrl+C to cancel
" -r null
|
||||||
|
openssl x509 \
|
||||||
|
-req \
|
||||||
|
-days 365 \
|
||||||
|
-in $(CSR) \
|
||||||
|
-copy_extensions copy \
|
||||||
|
-ext keyUsage,extendedKeyUsage \
|
||||||
|
-CA $(PKI_DIR)/ca.cert \
|
||||||
|
-CAkey $(PKI_DIR)/ca.key \
|
||||||
|
-CAcreateserial \
|
||||||
|
-out $@
|
||||||
|
echo >> $@
|
||||||
|
cat $(PKI_DIR)/ca.cert >> $@
|
||||||
|
openssl x509 -noout -text -in $@ -fingerprint -sha256
|
||||||
|
@echo "Certificate is ready.\nSend $@ back to maintainer."
|
||||||
|
|
||||||
|
$(PKI_DIR)/ca.key: CERT=$(patsubst %.key,%.cert,$@)
|
||||||
|
$(PKI_DIR)/ca.key:
|
||||||
|
openssl req \
|
||||||
|
-x509 \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-keyout $@ \
|
||||||
|
-out $(CERT) \
|
||||||
|
-sha256 \
|
||||||
|
-days 3650 \
|
||||||
|
-addext keyUsage=critical,keyCertSign \
|
||||||
|
-subj "/C=RU/O=TrueCloudLab/OU=TrueCloudLab/CN=TrueCloudLab Code Signing Certificate Authority"
|
||||||
|
@echo "IMPORTANT: Keep $@ private!\n"
|
||||||
|
endif
|
36
src/FrostFS.SDK.Client/ApeRules/Actions.cs
Normal file
36
src/FrostFS.SDK.Client/ApeRules/Actions.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public struct Actions(bool inverted, string[] names) : System.IEquatable<Actions>
|
||||||
|
{
|
||||||
|
public bool Inverted { get; set; } = inverted;
|
||||||
|
|
||||||
|
public string[] Names { get; set; } = names;
|
||||||
|
|
||||||
|
public override readonly bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null || obj is not Actions)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Equals((Actions)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override readonly int GetHashCode()
|
||||||
|
{
|
||||||
|
return Inverted.GetHashCode() ^ string.Join(string.Empty, Names).GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(Actions left, Actions right)
|
||||||
|
{
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(Actions left, Actions right)
|
||||||
|
{
|
||||||
|
return !(left == right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly bool Equals(Actions other)
|
||||||
|
{
|
||||||
|
return this.GetHashCode().Equals(other.GetHashCode());
|
||||||
|
}
|
||||||
|
}
|
43
src/FrostFS.SDK.Client/ApeRules/Condition.cs
Normal file
43
src/FrostFS.SDK.Client/ApeRules/Condition.cs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public struct Condition : System.IEquatable<Condition>
|
||||||
|
{
|
||||||
|
public ConditionType Op { get; set; }
|
||||||
|
|
||||||
|
public ConditionKindType Kind { get; set; }
|
||||||
|
|
||||||
|
public string? Key { get; set; }
|
||||||
|
|
||||||
|
public string? Value { get; set; }
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null || obj is not Condition)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Equals((Condition)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override readonly int GetHashCode()
|
||||||
|
{
|
||||||
|
return Op.GetHashCode()
|
||||||
|
^ Kind.GetHashCode()
|
||||||
|
^ (Key != null ? Key.GetHashCode() : 0)
|
||||||
|
^ (Value != null ? Value.GetHashCode() : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(Condition left, Condition right)
|
||||||
|
{
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(Condition left, Condition right)
|
||||||
|
{
|
||||||
|
return !(left == right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly bool Equals(Condition other)
|
||||||
|
{
|
||||||
|
return this.GetHashCode().Equals(other.GetHashCode());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public enum ConditionKindType
|
||||||
|
{
|
||||||
|
Resource,
|
||||||
|
Request
|
||||||
|
}
|
36
src/FrostFS.SDK.Client/ApeRules/Enums/ConditionType.cs
Normal file
36
src/FrostFS.SDK.Client/ApeRules/Enums/ConditionType.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public enum ConditionType
|
||||||
|
{
|
||||||
|
CondStringEquals,
|
||||||
|
|
||||||
|
CondStringNotEquals,
|
||||||
|
CondStringEqualsIgnoreCase,
|
||||||
|
|
||||||
|
CondStringNotEqualsIgnoreCase,
|
||||||
|
CondStringLike,
|
||||||
|
|
||||||
|
CondStringNotLike,
|
||||||
|
CondStringLessThan,
|
||||||
|
|
||||||
|
CondStringLessThanEquals,
|
||||||
|
CondStringGreaterThan,
|
||||||
|
|
||||||
|
CondStringGreaterThanEquals,
|
||||||
|
|
||||||
|
// Numeric condition operators.
|
||||||
|
CondNumericEquals,
|
||||||
|
|
||||||
|
CondNumericNotEquals,
|
||||||
|
CondNumericLessThan,
|
||||||
|
|
||||||
|
CondNumericLessThanEquals,
|
||||||
|
CondNumericGreaterThan,
|
||||||
|
|
||||||
|
CondNumericGreaterThanEquals,
|
||||||
|
|
||||||
|
CondSliceContains,
|
||||||
|
|
||||||
|
CondIPAddress,
|
||||||
|
CondNotIPAddress,
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
public enum FrostFsTargetType
|
public enum FrostFsTargetType
|
||||||
{
|
{
|
||||||
Undefined = 0,
|
Undefined,
|
||||||
Namespace,
|
Namespace,
|
||||||
Container,
|
Container,
|
||||||
User,
|
User,
|
9
src/FrostFS.SDK.Client/ApeRules/Enums/Status.cs
Normal file
9
src/FrostFS.SDK.Client/ApeRules/Enums/Status.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public enum RuleStatus
|
||||||
|
{
|
||||||
|
Allow,
|
||||||
|
NoRuleFound,
|
||||||
|
AccessDenied,
|
||||||
|
QuotaLimitReached
|
||||||
|
}
|
10
src/FrostFS.SDK.Client/ApeRules/FrostFsChain.cs
Normal file
10
src/FrostFS.SDK.Client/ApeRules/FrostFsChain.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public class FrostFsChain
|
||||||
|
{
|
||||||
|
public byte[] ID { get; set; } = [];
|
||||||
|
|
||||||
|
public FrostFsRule[] Rules { get; set; } = [];
|
||||||
|
|
||||||
|
public RuleMatchType MatchType { get; set; }
|
||||||
|
}
|
18
src/FrostFS.SDK.Client/ApeRules/FrostFsRule.cs
Normal file
18
src/FrostFS.SDK.Client/ApeRules/FrostFsRule.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public class FrostFsRule
|
||||||
|
{
|
||||||
|
public RuleStatus Status { get; set; }
|
||||||
|
|
||||||
|
// Actions the operation is applied to.
|
||||||
|
public Actions Actions { get; set; }
|
||||||
|
|
||||||
|
// List of the resources the operation is applied to.
|
||||||
|
public Resources Resources { get; set; }
|
||||||
|
|
||||||
|
// True iff individual conditions must be combined with the logical OR.
|
||||||
|
// By default AND is used, so _each_ condition must pass.
|
||||||
|
public bool Any { get; set; }
|
||||||
|
|
||||||
|
public Condition[]? Conditions { get; set; }
|
||||||
|
}
|
10
src/FrostFS.SDK.Client/ApeRules/MatchType.cs
Normal file
10
src/FrostFS.SDK.Client/ApeRules/MatchType.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public enum RuleMatchType
|
||||||
|
{
|
||||||
|
// DenyPriority rejects the request if any `Deny` is specified.
|
||||||
|
DenyPriority,
|
||||||
|
|
||||||
|
// FirstMatch returns the first rule action matched to the request.
|
||||||
|
FirstMatch
|
||||||
|
}
|
36
src/FrostFS.SDK.Client/ApeRules/Resources.cs
Normal file
36
src/FrostFS.SDK.Client/ApeRules/Resources.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public struct Resources(bool inverted, string[] names) : System.IEquatable<Resources>
|
||||||
|
{
|
||||||
|
public bool Inverted { get; set; } = inverted;
|
||||||
|
|
||||||
|
public string[] Names { get; set; } = names;
|
||||||
|
|
||||||
|
public override readonly bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null || obj is not Resources)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Equals((Resources)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override readonly int GetHashCode()
|
||||||
|
{
|
||||||
|
return Inverted.GetHashCode() ^ string.Join(string.Empty, Names).GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(Resources left, Resources right)
|
||||||
|
{
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(Resources left, Resources right)
|
||||||
|
{
|
||||||
|
return !(left == right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly bool Equals(Resources other)
|
||||||
|
{
|
||||||
|
return this.GetHashCode().Equals(other.GetHashCode());
|
||||||
|
}
|
||||||
|
}
|
502
src/FrostFS.SDK.Client/ApeRules/RuleSerializer.cs
Normal file
502
src/FrostFS.SDK.Client/ApeRules/RuleSerializer.cs
Normal file
|
@ -0,0 +1,502 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
internal static class RuleSerializer
|
||||||
|
{
|
||||||
|
const byte Version = 0; // increase if breaking change
|
||||||
|
const int ByteSize = 1;
|
||||||
|
const int UInt8Size = ByteSize;
|
||||||
|
const int BoolSize = ByteSize;
|
||||||
|
const long NullSlice = -1;
|
||||||
|
const int NullSliceSize = 1;
|
||||||
|
const byte ByteTrue = 1;
|
||||||
|
const byte ByteFalse = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// maxSliceLen taken from https://github.com/neo-project/neo/blob/38218bbee5bbe8b33cd8f9453465a19381c9a547/src/Neo/IO/Helper.cs#L77
|
||||||
|
/// </summary>
|
||||||
|
const int MaxSliceLen = 0x1000000;
|
||||||
|
|
||||||
|
const int ChainMarshalVersion = 0;
|
||||||
|
|
||||||
|
internal static byte[] Serialize(FrostFsChain chain)
|
||||||
|
{
|
||||||
|
int s = UInt8Size // Marshaller version
|
||||||
|
+ UInt8Size // Chain version
|
||||||
|
+ SliceSize(chain.ID, b => ByteSize)
|
||||||
|
+ SliceSize(chain.Rules, RuleSize)
|
||||||
|
+ UInt8Size; // MatchType
|
||||||
|
|
||||||
|
byte[] buf = new byte[s];
|
||||||
|
|
||||||
|
int offset = UInt8Marshal(buf, 0, Version);
|
||||||
|
offset = UInt8Marshal(buf, offset, ChainMarshalVersion);
|
||||||
|
offset = SliceMarshal(buf, offset, chain.ID, ByteMarshal);
|
||||||
|
offset = SliceMarshal(buf, offset, chain.Rules, MarshalRule);
|
||||||
|
offset = UInt8Marshal(buf, offset, (byte)chain.MatchType);
|
||||||
|
|
||||||
|
VerifyMarshal(buf, offset);
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static FrostFsChain Deserialize(byte[] data)
|
||||||
|
{
|
||||||
|
if (data is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
FrostFsChain chain = new();
|
||||||
|
|
||||||
|
var (offset, version) = UInt8Unmarshal(data, 0);
|
||||||
|
|
||||||
|
if (version != Version)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"unsupported marshaller version {version}");
|
||||||
|
}
|
||||||
|
|
||||||
|
(offset, byte chainVersion) = UInt8Unmarshal(data, offset);
|
||||||
|
|
||||||
|
if (chainVersion != ChainMarshalVersion)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"unsupported chain version {chainVersion}");
|
||||||
|
}
|
||||||
|
|
||||||
|
(offset, chain.ID) = SliceUnmarshal(data, offset, UInt8Unmarshal);
|
||||||
|
|
||||||
|
(offset, chain.Rules) = SliceUnmarshal(data, offset, UnmarshalRule);
|
||||||
|
|
||||||
|
(offset, var matchTypeV) = UInt8Unmarshal(data, offset);
|
||||||
|
|
||||||
|
chain.MatchType = (RuleMatchType)matchTypeV;
|
||||||
|
|
||||||
|
VerifyUnmarshal(data, offset);
|
||||||
|
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Int64Size(long value)
|
||||||
|
{
|
||||||
|
// https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=92;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c
|
||||||
|
// and
|
||||||
|
// https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=41;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c
|
||||||
|
ulong ux = (ulong)value << 1;
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
ux = ~ux;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size = 0;
|
||||||
|
while (ux >= 0x80)
|
||||||
|
{
|
||||||
|
size++;
|
||||||
|
ux >>= 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SliceSize<T>(T[] slice, Func<T, int> sizeOf)
|
||||||
|
{
|
||||||
|
if (slice == null)
|
||||||
|
{
|
||||||
|
return NullSliceSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming Int64Size is the size of the slice
|
||||||
|
var size = Int64Size(slice.Length);
|
||||||
|
foreach (var v in slice)
|
||||||
|
{
|
||||||
|
size += sizeOf(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int StringSize(string? s)
|
||||||
|
{
|
||||||
|
var len = s != null ? s.Length : 0;
|
||||||
|
return Int64Size(len) + len;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ActionsSize(Actions action)
|
||||||
|
{
|
||||||
|
return BoolSize // Inverted
|
||||||
|
+ SliceSize(action.Names, StringSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ResourcesSize(Resources resource)
|
||||||
|
{
|
||||||
|
return BoolSize // Inverted
|
||||||
|
+ SliceSize(resource.Names, StringSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ConditionSize(Condition condition)
|
||||||
|
{
|
||||||
|
return ByteSize // Op
|
||||||
|
+ ByteSize // Object
|
||||||
|
+ StringSize(condition.Key)
|
||||||
|
+ StringSize(condition.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RuleSize(FrostFsRule rule)
|
||||||
|
{
|
||||||
|
if (rule is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(rule));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ByteSize // Status
|
||||||
|
+ ActionsSize(rule.Actions)
|
||||||
|
+ ResourcesSize(rule.Resources)
|
||||||
|
+ BoolSize // Any
|
||||||
|
+ SliceSize(rule.Conditions!, ConditionSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int UInt8Marshal(byte[] buf, int offset, byte value)
|
||||||
|
{
|
||||||
|
if (buf.Length - offset < 1)
|
||||||
|
{
|
||||||
|
throw new FrostFsException("Not enough bytes left to serialize value of type byte");
|
||||||
|
}
|
||||||
|
|
||||||
|
buf[offset] = value;
|
||||||
|
|
||||||
|
return offset + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ByteMarshal(byte[] buf, int offset, byte value)
|
||||||
|
{
|
||||||
|
return UInt8Marshal(buf, offset, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutVarint encodes an int64 into buf and returns the number of bytes written.
|
||||||
|
// If the buffer is too small, PutVarint will panic.
|
||||||
|
private static int PutVarint(byte[] buf, int offset, long x)
|
||||||
|
{
|
||||||
|
var ux = (ulong)x << 1;
|
||||||
|
|
||||||
|
if (x < 0)
|
||||||
|
{
|
||||||
|
ux = ~ux;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PutUvarint(buf, offset, ux);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PutUvarint(byte[] buf, int offset, ulong x)
|
||||||
|
{
|
||||||
|
while (x >= 0x80)
|
||||||
|
{
|
||||||
|
buf[offset] = (byte)(x | 0x80);
|
||||||
|
x >>= 7;
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf[offset] = (byte)x;
|
||||||
|
|
||||||
|
return offset + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Int64Marshal(byte[] buf, int offset, long v)
|
||||||
|
{
|
||||||
|
if (buf.Length - offset < Int64Size(v))
|
||||||
|
{
|
||||||
|
throw new FrostFsException("Not enough bytes left to serialize value of type long");
|
||||||
|
}
|
||||||
|
|
||||||
|
return PutVarint(buf, offset, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SliceMarshal<T>(byte[] buf, int offset, T[] slice, Func<byte[], int, T, int> marshalT)
|
||||||
|
{
|
||||||
|
if (slice == null)
|
||||||
|
{
|
||||||
|
return Int64Marshal(buf, offset, NullSlice);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slice.Length > MaxSliceLen)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"slice size if too big: {slice.Length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = Int64Marshal(buf, offset, slice.Length);
|
||||||
|
|
||||||
|
foreach (var v in slice)
|
||||||
|
{
|
||||||
|
offset = marshalT(buf, offset, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int BoolMarshal(byte[] buf, int offset, bool value)
|
||||||
|
{
|
||||||
|
return UInt8Marshal(buf, offset, value ? ByteTrue : ByteFalse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int StringMarshal(byte[] buf, int offset, string value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"string value is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Length > MaxSliceLen)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"string is too long: {value.Length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.Length - offset < Int64Size(value.Length) + value.Length)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"Not enough bytes left to serialize value of type string with length {value.Length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = Int64Marshal(buf, offset, value.Length);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer.BlockCopy(System.Text.Encoding.UTF8.GetBytes(value), 0, buf, offset, value.Length);
|
||||||
|
|
||||||
|
return offset + value.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MarshalActions(byte[] buf, int offset, Actions action)
|
||||||
|
{
|
||||||
|
offset = BoolMarshal(buf, offset, action.Inverted);
|
||||||
|
|
||||||
|
return SliceMarshal(buf, offset, action.Names, StringMarshal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MarshalCondition(byte[] buf, int offset, Condition condition)
|
||||||
|
{
|
||||||
|
offset = ByteMarshal(buf, offset, (byte)condition.Op);
|
||||||
|
|
||||||
|
offset = ByteMarshal(buf, offset, (byte)condition.Kind);
|
||||||
|
|
||||||
|
offset = StringMarshal(buf, offset, condition.Key!);
|
||||||
|
|
||||||
|
return StringMarshal(buf, offset, condition.Value!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MarshalRule(byte[] buf, int offset, FrostFsRule rule)
|
||||||
|
{
|
||||||
|
if (rule is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(rule));
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = ByteMarshal(buf, offset, (byte)rule.Status);
|
||||||
|
|
||||||
|
offset = MarshalActions(buf, offset, rule.Actions);
|
||||||
|
|
||||||
|
offset = MarshalResources(buf, offset, rule.Resources);
|
||||||
|
|
||||||
|
offset = BoolMarshal(buf, offset, rule.Any);
|
||||||
|
|
||||||
|
return SliceMarshal(buf, offset, rule.Conditions!, MarshalCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MarshalResources(byte[] buf, int offset, Resources resources)
|
||||||
|
{
|
||||||
|
offset = BoolMarshal(buf, offset, resources.Inverted);
|
||||||
|
|
||||||
|
return SliceMarshal(buf, offset, resources.Names, StringMarshal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VerifyMarshal(byte[] buf, int lastOffset)
|
||||||
|
{
|
||||||
|
if (buf.Length != lastOffset)
|
||||||
|
{
|
||||||
|
throw new FrostFsException("actual data size differs from expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, bool) BoolUnmarshal(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
(offset, byte val) = UInt8Unmarshal(buf, offset);
|
||||||
|
return (offset, val == ByteTrue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, string) StringUnmarshal(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
(offset, long size) = Int64Unmarshal(buf, offset);
|
||||||
|
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return (offset, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size > MaxSliceLen)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"string is too long: '{size}'");
|
||||||
|
}
|
||||||
|
if (size < 0)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"invalid string size: '{size}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.Length - offset < size)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"not enough bytes left to string value");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (offset + (int)size, System.Text.Encoding.UTF8.GetString(buf, offset, (int)size));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, Actions) UnmarshalActions(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
Actions action = new();
|
||||||
|
(offset, action.Inverted) = BoolUnmarshal(buf, offset);
|
||||||
|
|
||||||
|
(offset, action.Names) = SliceUnmarshal(buf, offset, StringUnmarshal);
|
||||||
|
|
||||||
|
return (offset, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, Resources) UnmarshalResources(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
Resources res = new();
|
||||||
|
|
||||||
|
(offset, res.Inverted) = BoolUnmarshal(buf, offset);
|
||||||
|
(offset, res.Names) = SliceUnmarshal(buf, offset, StringUnmarshal);
|
||||||
|
|
||||||
|
return (offset, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, Condition) UnmarshalCondition(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
Condition cond = new();
|
||||||
|
(offset, var op) = UInt8Unmarshal(buf, offset);
|
||||||
|
|
||||||
|
cond.Op = (ConditionType)op;
|
||||||
|
|
||||||
|
(offset, var kind) = UInt8Unmarshal(buf, offset);
|
||||||
|
|
||||||
|
cond.Kind = (ConditionKindType)kind;
|
||||||
|
|
||||||
|
(offset, cond.Key) = StringUnmarshal(buf, offset);
|
||||||
|
|
||||||
|
(offset, cond.Value) = StringUnmarshal(buf, offset);
|
||||||
|
|
||||||
|
return (offset, cond);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, FrostFsRule) UnmarshalRule(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
FrostFsRule rule = new();
|
||||||
|
|
||||||
|
(offset, byte statusV) = UInt8Unmarshal(buf, offset);
|
||||||
|
rule.Status = (RuleStatus)statusV;
|
||||||
|
|
||||||
|
(offset, rule.Actions) = UnmarshalActions(buf, offset);
|
||||||
|
|
||||||
|
(offset, rule.Resources) = UnmarshalResources(buf, offset);
|
||||||
|
|
||||||
|
(offset, rule.Any) = BoolUnmarshal(buf, offset);
|
||||||
|
|
||||||
|
(offset, rule.Conditions) = SliceUnmarshal(buf, offset, UnmarshalCondition);
|
||||||
|
|
||||||
|
return (offset, rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, byte) UInt8Unmarshal(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
if (buf.Length - offset < 1)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"not enough bytes left to read a value of type 'byte' from offset {offset}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (offset + 1, buf[offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, long) Int64Unmarshal(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
if (buf.Length - offset < sizeof(long))
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"not enough bytes left to read a value of type 'long' from offset {offset}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Varint(buf, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, T[]) SliceUnmarshal<T>(byte[] buf, int offset, Func<byte[], int, (int, T)> unmarshalT)
|
||||||
|
{
|
||||||
|
var (newOffset, size) = Varint(buf, offset);
|
||||||
|
|
||||||
|
if (size == NullSlice)
|
||||||
|
{
|
||||||
|
return (newOffset, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size > MaxSliceLen)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"slice size is too big: '{size}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size < 0)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"invalid slice size: '{size}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new T[size];
|
||||||
|
for (int i = 0; i < result.Length; i++)
|
||||||
|
{
|
||||||
|
(newOffset, result[i]) = unmarshalT(buf, newOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (newOffset, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VerifyUnmarshal(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
if (buf.Length != offset)
|
||||||
|
{
|
||||||
|
throw new FrostFsException("unmarshalled bytes left");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MaxVarIntLen64 = 10;
|
||||||
|
|
||||||
|
public static (int, long) Varint(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
var (ux, n) = Uvarint(buf, offset); // ok to continue in presence of error
|
||||||
|
long x = (long)ux >> 1;
|
||||||
|
if ((ux & 1) != 0)
|
||||||
|
{
|
||||||
|
x = ~x;
|
||||||
|
}
|
||||||
|
return (n, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (ulong, int) Uvarint(byte[] buf, int offset)
|
||||||
|
{
|
||||||
|
ulong x = 0;
|
||||||
|
int s = 0;
|
||||||
|
|
||||||
|
for (int i = offset; i < buf.Length; i++)
|
||||||
|
{
|
||||||
|
byte b = buf[i];
|
||||||
|
if (i == MaxVarIntLen64)
|
||||||
|
{
|
||||||
|
return (0, -(i + 1)); // overflow
|
||||||
|
}
|
||||||
|
if (b < 0x80)
|
||||||
|
{
|
||||||
|
if (i == MaxVarIntLen64 - 1 && b > 1)
|
||||||
|
{
|
||||||
|
return (0, -(i + 1)); // overflow
|
||||||
|
}
|
||||||
|
return (x | ((ulong)b << s), i + 1);
|
||||||
|
}
|
||||||
|
x |= (ulong)(b & 0x7f) << s;
|
||||||
|
s += 7;
|
||||||
|
}
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
}
|
14
src/FrostFS.SDK.Client/AssemblyInfo.cs
Normal file
14
src/FrostFS.SDK.Client/AssemblyInfo.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: AssemblyCompany("TrueCloudLab")]
|
||||||
|
[assembly: InternalsVisibleTo("FrostFS.SDK.Tests, PublicKey=" +
|
||||||
|
"002400000480000094000000060200000024000052534131000400000100010089b992fb14ebcf"+
|
||||||
|
"4b85b4b1a3af1897290c52ff85a106035c47dc5604cbaa58ae3180f5c9b8523fee5dd1bb9ea9cf"+
|
||||||
|
"e15ab287e6239c98d5dfa91615bd77485d523a3a3f65a4e5028454cedd5ac4d9eca6da18b81985"+
|
||||||
|
"ac6905d33cc64b5a2587050c16f67b71ef8889dbd3c90ef7cc0b06bbbe09886601d195f5db179a"+
|
||||||
|
"3c2a25b1")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.6.0")]
|
||||||
|
[assembly: AssemblyProduct("FrostFS.SDK.Client")]
|
||||||
|
[assembly: AssemblyTitle("FrostFS.SDK.Client")]
|
||||||
|
[assembly: AssemblyVersion("1.0.6")]
|
|
@ -6,17 +6,8 @@ internal static class Caches
|
||||||
{
|
{
|
||||||
private static readonly IMemoryCache _ownersCache = new MemoryCache(new MemoryCacheOptions
|
private static readonly IMemoryCache _ownersCache = new MemoryCache(new MemoryCacheOptions
|
||||||
{
|
{
|
||||||
// TODO: get from options?
|
|
||||||
SizeLimit = 256
|
SizeLimit = 256
|
||||||
});
|
});
|
||||||
|
|
||||||
private static readonly IMemoryCache _containersCache = new MemoryCache(new MemoryCacheOptions
|
|
||||||
{
|
|
||||||
// TODO: get from options?
|
|
||||||
SizeLimit = 1024
|
|
||||||
});
|
|
||||||
|
|
||||||
internal static IMemoryCache Owners => _ownersCache;
|
internal static IMemoryCache Owners => _ownersCache;
|
||||||
|
|
||||||
internal static IMemoryCache Containers => _containersCache;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ public class ClientKey(ECDsa key)
|
||||||
|
|
||||||
internal ByteString PublicKeyProto { get; } = ByteString.CopyFrom(key.PublicKey());
|
internal ByteString PublicKeyProto { get; } = ByteString.CopyFrom(key.PublicKey());
|
||||||
|
|
||||||
internal string PublicKey { get; } = key.PublicKey().ToString();
|
internal string PublicKey { get; } = Base58.Encode(key.PublicKey());
|
||||||
|
|
||||||
internal FrostFsOwner Owner { get; } = new FrostFsOwner(key.PublicKey().PublicKeyToAddress());
|
internal FrostFsOwner Owner { get; } = new FrostFsOwner(key.PublicKey().PublicKeyToAddress());
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ public class FrostFsResponseException : FrostFsException
|
||||||
}
|
}
|
||||||
|
|
||||||
public FrostFsResponseException(FrostFsResponseStatus status)
|
public FrostFsResponseException(FrostFsResponseStatus status)
|
||||||
|
: base(status != null ? status.Message != null ? "" : "" : "")
|
||||||
{
|
{
|
||||||
Status = status;
|
Status = status;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using Google.Protobuf;
|
using Google.Protobuf;
|
||||||
|
|
||||||
namespace FrostFS.SDK.Cryptography;
|
namespace FrostFS.SDK.Cryptography;
|
||||||
|
|
||||||
public static class FrostFsExtensions
|
public static class FrostFsExtensions
|
||||||
{
|
{
|
||||||
public static ByteString Sha256(this IMessage data)
|
public static byte[] Sha256(this IMessage data)
|
||||||
{
|
{
|
||||||
return ByteString.CopyFrom(data.ToByteArray().Sha256());
|
using var sha256 = SHA256.Create();
|
||||||
|
using HashStream stream = new(sha256);
|
||||||
|
data.WriteTo(stream);
|
||||||
|
return stream.Hash();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Guid ToUuid(this ByteString id)
|
public static Guid ToUuid(this ByteString id)
|
||||||
|
@ -16,9 +19,18 @@ public static class FrostFsExtensions
|
||||||
if (id == null)
|
if (id == null)
|
||||||
throw new ArgumentNullException(nameof(id));
|
throw new ArgumentNullException(nameof(id));
|
||||||
|
|
||||||
var orderedBytes = GetGuidBytesDirectOrder(id.Span);
|
return new Guid(
|
||||||
|
(id[0] << 24) + (id[1] << 16) + (id[2] << 8) + id[3],
|
||||||
return new Guid(orderedBytes);
|
(short)((id[4] << 8) + id[5]),
|
||||||
|
(short)((id[6] << 8) + id[7]),
|
||||||
|
id[8],
|
||||||
|
id[9],
|
||||||
|
id[10],
|
||||||
|
id[11],
|
||||||
|
id[12],
|
||||||
|
id[13],
|
||||||
|
id[14],
|
||||||
|
id[15]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -26,37 +38,25 @@ public static class FrostFsExtensions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static byte[] ToBytes(this Guid id)
|
public unsafe static void ToBytes(this Guid id, Span<byte> span)
|
||||||
{
|
{
|
||||||
var bytes = id.ToByteArray();
|
var pGuid = (byte*)&id;
|
||||||
|
|
||||||
var orderedBytes = GetGuidBytesDirectOrder(bytes);
|
span[0] = pGuid[3];
|
||||||
|
span[1] = pGuid[2];
|
||||||
return orderedBytes;
|
span[2] = pGuid[1];
|
||||||
}
|
span[3] = pGuid[0];
|
||||||
|
span[4] = pGuid[5];
|
||||||
private static byte[] GetGuidBytesDirectOrder(ReadOnlySpan<byte> source)
|
span[5] = pGuid[4];
|
||||||
{
|
span[6] = pGuid[7];
|
||||||
if (source.Length != 16)
|
span[7] = pGuid[6];
|
||||||
throw new ArgumentException("Wrong uuid binary format");
|
span[8] = pGuid[8];
|
||||||
|
span[9] = pGuid[9];
|
||||||
return [
|
span[10] = pGuid[10];
|
||||||
source[3],
|
span[11] = pGuid[11];
|
||||||
source[2],
|
span[12] = pGuid[12];
|
||||||
source[1],
|
span[13] = pGuid[13];
|
||||||
source[0],
|
span[14] = pGuid[14];
|
||||||
source[5],
|
span[15] = pGuid[15];
|
||||||
source[4],
|
|
||||||
source[7],
|
|
||||||
source[6],
|
|
||||||
source[8],
|
|
||||||
source[9],
|
|
||||||
source[10],
|
|
||||||
source[11],
|
|
||||||
source[12],
|
|
||||||
source[13],
|
|
||||||
source[14],
|
|
||||||
source[15]
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,36 @@
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<LangVersion>12.0</LangVersion>
|
<LangVersion>12.0</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||||
|
<PackageId>FrostFS.SDK.Client</PackageId>
|
||||||
|
<Version>1.0.6</Version>
|
||||||
|
<Description>
|
||||||
|
C# SDK for FrostFS gRPC native protocol
|
||||||
|
</Description>
|
||||||
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
|
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_SkipUpgradeNetAnalyzersNuGetWarning>true</_SkipUpgradeNetAnalyzersNuGetWarning>
|
<_SkipUpgradeNetAnalyzersNuGetWarning>true</_SkipUpgradeNetAnalyzersNuGetWarning>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
|
<SignAssembly>True</SignAssembly>
|
||||||
|
<AssemblyOriginatorKeyFile>.\\..\\..\\keyfile.snk</AssemblyOriginatorKeyFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -34,6 +47,7 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
||||||
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="7.0.0" />
|
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="7.0.0" />
|
||||||
<PackageReference Include="System.Runtime.Caching" Version="7.0.0" />
|
<PackageReference Include="System.Runtime.Caching" Version="7.0.0" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="7.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Frostfs.V2.Ape;
|
|
||||||
|
|
||||||
using FrostFS.SDK.Client.Interfaces;
|
using FrostFS.SDK.Client.Interfaces;
|
||||||
using FrostFS.SDK.Client.Services;
|
using FrostFS.SDK.Client.Services;
|
||||||
using FrostFS.SDK.Cryptography;
|
using FrostFS.SDK.Cryptography;
|
||||||
|
@ -164,24 +162,6 @@ public class FrostFSClient : IFrostFSClient
|
||||||
Callback = settings.Value.Callback,
|
Callback = settings.Value.Callback,
|
||||||
Interceptors = settings.Value.Interceptors
|
Interceptors = settings.Value.Interceptors
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: define timeout logic
|
|
||||||
// CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20) });
|
|
||||||
}
|
|
||||||
|
|
||||||
internal FrostFSClient(WrapperPrm prm, SessionCache cache)
|
|
||||||
{
|
|
||||||
ClientCtx = new ClientContext(
|
|
||||||
client: this,
|
|
||||||
key: new ClientKey(prm.Key),
|
|
||||||
owner: FrostFsOwner.FromKey(prm.Key!),
|
|
||||||
channel: prm.GrpcChannelFactory(prm.Address),
|
|
||||||
version: new FrostFsVersion(2, 13))
|
|
||||||
{
|
|
||||||
SessionCache = cache,
|
|
||||||
Interceptors = prm.Interceptors,
|
|
||||||
Callback = prm.Callback
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region ApeManagerImplementation
|
#region ApeManagerImplementation
|
||||||
|
@ -195,7 +175,7 @@ public class FrostFSClient : IFrostFSClient
|
||||||
return GetApeManagerService().RemoveChainAsync(args, ctx);
|
return GetApeManagerService().RemoveChainAsync(args, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Chain[]> ListChainAsync(PrmApeChainList args, CallContext ctx)
|
public Task<FrostFsChain[]> ListChainAsync(PrmApeChainList args, CallContext ctx)
|
||||||
{
|
{
|
||||||
return GetApeManagerService().ListChainAsync(args, ctx);
|
return GetApeManagerService().ListChainAsync(args, ctx);
|
||||||
}
|
}
|
||||||
|
@ -212,9 +192,15 @@ public class FrostFSClient : IFrostFSClient
|
||||||
return GetContainerService().ListContainersAsync(args, ctx);
|
return GetContainerService().ListContainersAsync(args, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Obsolete("Use PutContainerAsync method")]
|
||||||
public Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args, CallContext ctx)
|
public Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args, CallContext ctx)
|
||||||
{
|
{
|
||||||
return GetContainerService().CreateContainerAsync(args, ctx);
|
return GetContainerService().PutContainerAsync(args, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<FrostFsContainerId> PutContainerAsync(PrmContainerCreate args, CallContext ctx)
|
||||||
|
{
|
||||||
|
return GetContainerService().PutContainerAsync(args, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx)
|
public Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx)
|
||||||
|
@ -268,7 +254,8 @@ public class FrostFSClient : IFrostFSClient
|
||||||
|
|
||||||
public Task<FrostFsObjectId> PutClientCutObjectAsync(PrmObjectClientCutPut args, CallContext ctx)
|
public Task<FrostFsObjectId> PutClientCutObjectAsync(PrmObjectClientCutPut args, CallContext ctx)
|
||||||
{
|
{
|
||||||
return GetObjectService().PutClientCutObjectAsync(args, ctx);
|
return GetObjectService().PutClientCutSingleObjectAsync(args, ctx);
|
||||||
|
// return GetObjectService().PutClientCutObjectAsync(args, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<FrostFsObjectId> PutSingleObjectAsync(PrmSingleObjectPut args, CallContext ctx)
|
public Task<FrostFsObjectId> PutSingleObjectAsync(PrmSingleObjectPut args, CallContext ctx)
|
||||||
|
@ -314,18 +301,6 @@ public class FrostFSClient : IFrostFSClient
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async void CheckFrostFsVersionSupport(CallContext ctx)
|
|
||||||
{
|
|
||||||
var service = GetNetmapService();
|
|
||||||
var localNodeInfo = await service.GetLocalNodeInfoAsync(ctx).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!localNodeInfo.Version.IsSupported(ClientCtx.Version))
|
|
||||||
{
|
|
||||||
var msg = $"FrostFS {localNodeInfo.Version} is not supported.";
|
|
||||||
throw new FrostFsException(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CallInvoker? CreateInvoker()
|
private CallInvoker? CreateInvoker()
|
||||||
{
|
{
|
||||||
CallInvoker? callInvoker = null;
|
CallInvoker? callInvoker = null;
|
||||||
|
@ -361,7 +336,7 @@ public class FrostFSClient : IFrostFSClient
|
||||||
{
|
{
|
||||||
var invoker = CreateInvoker();
|
var invoker = CreateInvoker();
|
||||||
|
|
||||||
NetmapServiceClient = NetmapServiceClient ?? (
|
NetmapServiceClient ??= (
|
||||||
invoker != null
|
invoker != null
|
||||||
? new NetmapServiceClient(invoker)
|
? new NetmapServiceClient(invoker)
|
||||||
: new NetmapServiceClient(ClientCtx.Channel));
|
: new NetmapServiceClient(ClientCtx.Channel));
|
||||||
|
@ -378,7 +353,7 @@ public class FrostFSClient : IFrostFSClient
|
||||||
{
|
{
|
||||||
var invoker = CreateInvoker();
|
var invoker = CreateInvoker();
|
||||||
|
|
||||||
SessionServiceClient = SessionServiceClient ?? (
|
SessionServiceClient ??= (
|
||||||
invoker != null
|
invoker != null
|
||||||
? new SessionServiceClient(invoker)
|
? new SessionServiceClient(invoker)
|
||||||
: new SessionServiceClient(ClientCtx.Channel));
|
: new SessionServiceClient(ClientCtx.Channel));
|
||||||
|
@ -387,7 +362,6 @@ public class FrostFSClient : IFrostFSClient
|
||||||
}
|
}
|
||||||
|
|
||||||
return SessionServiceProvider;
|
return SessionServiceProvider;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ApeManagerServiceProvider GetApeManagerService()
|
private ApeManagerServiceProvider GetApeManagerService()
|
||||||
|
@ -396,7 +370,7 @@ public class FrostFSClient : IFrostFSClient
|
||||||
{
|
{
|
||||||
var invoker = CreateInvoker();
|
var invoker = CreateInvoker();
|
||||||
|
|
||||||
ApeManagerServiceClient = ApeManagerServiceClient ?? (
|
ApeManagerServiceClient ??= (
|
||||||
invoker != null
|
invoker != null
|
||||||
? new APEManagerServiceClient(invoker)
|
? new APEManagerServiceClient(invoker)
|
||||||
: new APEManagerServiceClient(ClientCtx.Channel));
|
: new APEManagerServiceClient(ClientCtx.Channel));
|
||||||
|
@ -413,7 +387,7 @@ public class FrostFSClient : IFrostFSClient
|
||||||
{
|
{
|
||||||
var invoker = CreateInvoker();
|
var invoker = CreateInvoker();
|
||||||
|
|
||||||
AccountingServiceClient = AccountingServiceClient ?? (
|
AccountingServiceClient ??= (
|
||||||
invoker != null
|
invoker != null
|
||||||
? new AccountingServiceClient(invoker)
|
? new AccountingServiceClient(invoker)
|
||||||
: new AccountingServiceClient(ClientCtx.Channel));
|
: new AccountingServiceClient(ClientCtx.Channel));
|
||||||
|
@ -430,7 +404,7 @@ public class FrostFSClient : IFrostFSClient
|
||||||
{
|
{
|
||||||
var invoker = CreateInvoker();
|
var invoker = CreateInvoker();
|
||||||
|
|
||||||
ContainerServiceClient = ContainerServiceClient ?? (
|
ContainerServiceClient ??= (
|
||||||
invoker != null
|
invoker != null
|
||||||
? new ContainerServiceClient(invoker)
|
? new ContainerServiceClient(invoker)
|
||||||
: new ContainerServiceClient(ClientCtx.Channel));
|
: new ContainerServiceClient(ClientCtx.Channel));
|
||||||
|
@ -447,7 +421,7 @@ public class FrostFSClient : IFrostFSClient
|
||||||
{
|
{
|
||||||
var invoker = CreateInvoker();
|
var invoker = CreateInvoker();
|
||||||
|
|
||||||
ObjectServiceClient = ObjectServiceClient ?? (
|
ObjectServiceClient ??= (
|
||||||
invoker != null
|
invoker != null
|
||||||
? new ObjectServiceClient(invoker)
|
? new ObjectServiceClient(invoker)
|
||||||
: new ObjectServiceClient(ClientCtx.Channel));
|
: new ObjectServiceClient(ClientCtx.Channel));
|
||||||
|
@ -457,17 +431,4 @@ public class FrostFSClient : IFrostFSClient
|
||||||
|
|
||||||
return ObjectServiceProvider;
|
return ObjectServiceProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> Dial(CallContext ctx)
|
|
||||||
{
|
|
||||||
var service = GetAccouningService();
|
|
||||||
_ = await service.GetBallance(ctx).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool RestartIfUnhealthy(CallContext ctx)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Frostfs.V2.Ape;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client.Interfaces;
|
namespace FrostFS.SDK.Client.Interfaces;
|
||||||
|
|
||||||
public interface IFrostFSClient
|
public interface IFrostFSClient
|
||||||
|
@ -25,7 +23,7 @@ public interface IFrostFSClient
|
||||||
|
|
||||||
Task RemoveChainAsync(PrmApeChainRemove args, CallContext ctx);
|
Task RemoveChainAsync(PrmApeChainRemove args, CallContext ctx);
|
||||||
|
|
||||||
Task<Chain[]> ListChainAsync(PrmApeChainList args, CallContext ctx);
|
Task<FrostFsChain[]> ListChainAsync(PrmApeChainList args, CallContext ctx);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Container
|
#region Container
|
||||||
|
@ -33,8 +31,11 @@ public interface IFrostFSClient
|
||||||
|
|
||||||
IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll args, CallContext ctx);
|
IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll args, CallContext ctx);
|
||||||
|
|
||||||
|
[Obsolete("Use PutContainerAsync method")]
|
||||||
Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args, CallContext ctx);
|
Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args, CallContext ctx);
|
||||||
|
|
||||||
|
Task<FrostFsContainerId> PutContainerAsync(PrmContainerCreate args, CallContext ctx);
|
||||||
|
|
||||||
Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx);
|
Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -63,6 +64,4 @@ public interface IFrostFSClient
|
||||||
#region Account
|
#region Account
|
||||||
Task<Accounting.Decimal> GetBalanceAsync(CallContext ctx);
|
Task<Accounting.Decimal> GetBalanceAsync(CallContext ctx);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public Task<string?> Dial(CallContext ctx);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,10 @@ using FrostFS.SDK.Cryptography;
|
||||||
|
|
||||||
using Google.Protobuf;
|
using Google.Protobuf;
|
||||||
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client.Mappers.GRPC;
|
namespace FrostFS.SDK.Client.Mappers.GRPC;
|
||||||
|
|
||||||
public static class ContainerIdMapper
|
public static class ContainerIdMapper
|
||||||
{
|
{
|
||||||
private static readonly MemoryCacheEntryOptions _oneHourExpiration = new MemoryCacheEntryOptions()
|
|
||||||
.SetSlidingExpiration(TimeSpan.FromHours(1))
|
|
||||||
.SetSize(1);
|
|
||||||
|
|
||||||
public static ContainerID ToMessage(this FrostFsContainerId model)
|
public static ContainerID ToMessage(this FrostFsContainerId model)
|
||||||
{
|
{
|
||||||
if (model is null)
|
if (model is null)
|
||||||
|
@ -24,15 +18,11 @@ public static class ContainerIdMapper
|
||||||
|
|
||||||
var containerId = model.GetValue() ?? throw new ArgumentNullException(nameof(model));
|
var containerId = model.GetValue() ?? throw new ArgumentNullException(nameof(model));
|
||||||
|
|
||||||
if (!Caches.Containers.TryGetValue(containerId, out ContainerID? message))
|
var message = new ContainerID
|
||||||
{
|
{
|
||||||
message = new ContainerID
|
Value = UnsafeByteOperations.UnsafeWrap(Base58.Decode(containerId))
|
||||||
{
|
};
|
||||||
Value = ByteString.CopyFrom(Base58.Decode(containerId))
|
|
||||||
};
|
|
||||||
|
|
||||||
Caches.Containers.Set(containerId, message, _oneHourExpiration);
|
|
||||||
}
|
|
||||||
return message!;
|
return message!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,10 @@ public static class PlacementPolicyMapper
|
||||||
|
|
||||||
return new FrostFsPlacementPolicy(
|
return new FrostFsPlacementPolicy(
|
||||||
placementPolicy.Unique,
|
placementPolicy.Unique,
|
||||||
placementPolicy.Replicas.Select(replica => replica.ToModel()).ToArray()
|
placementPolicy.ContainerBackupFactor,
|
||||||
|
[.. placementPolicy.Selectors.Select(s => s.ToModel())],
|
||||||
|
[.. placementPolicy.Filters.Select(f => f.ToModel())],
|
||||||
|
[.. placementPolicy.Replicas.Select(r => r.ToModel())]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,17 +1,20 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
using FrostFS.Netmap;
|
using FrostFS.Netmap;
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
public static class ReplicaMapper
|
public static class PolicyMapper
|
||||||
{
|
{
|
||||||
public static Replica ToMessage(this FrostFsReplica replica)
|
public static Replica ToMessage(this FrostFsReplica replica)
|
||||||
{
|
{
|
||||||
return new Replica
|
return new Replica
|
||||||
{
|
{
|
||||||
Count = (uint)replica.Count,
|
Count = (uint)replica.Count,
|
||||||
Selector = replica.Selector
|
Selector = replica.Selector,
|
||||||
|
EcDataCount = replica.EcDataCount,
|
||||||
|
EcParityCount = replica.EcParityCount
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +25,82 @@ public static class ReplicaMapper
|
||||||
throw new ArgumentNullException(nameof(replica));
|
throw new ArgumentNullException(nameof(replica));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FrostFsReplica((int)replica.Count, replica.Selector);
|
return new FrostFsReplica((int)replica.Count, replica.Selector)
|
||||||
|
{
|
||||||
|
EcDataCount = replica.EcDataCount,
|
||||||
|
EcParityCount = replica.EcParityCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Selector ToMessage(this FrostFsSelector selector)
|
||||||
|
{
|
||||||
|
if (selector is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Selector
|
||||||
|
{
|
||||||
|
Name = selector.Name,
|
||||||
|
Count = selector.Count,
|
||||||
|
Clause = (Clause)selector.Clause,
|
||||||
|
Attribute = selector.Attribute,
|
||||||
|
Filter = selector.Filter
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FrostFsSelector ToModel(this Selector selector)
|
||||||
|
{
|
||||||
|
if (selector is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = new FrostFsSelector(selector.Name)
|
||||||
|
{
|
||||||
|
Count = selector.Count,
|
||||||
|
Clause = (int)selector.Clause,
|
||||||
|
Attribute = selector.Attribute,
|
||||||
|
Filter = selector.Filter
|
||||||
|
};
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Filter ToMessage(this FrostFsFilter filter)
|
||||||
|
{
|
||||||
|
if (filter is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = new Filter
|
||||||
|
{
|
||||||
|
Name = filter.Name,
|
||||||
|
Key = filter.Key,
|
||||||
|
Op = (Operation)filter.Operation,
|
||||||
|
Value = filter.Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
message.Filters.AddRange(filter.Filters.Select(f => f.ToMessage()));
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FrostFsFilter ToModel(this Filter filter)
|
||||||
|
{
|
||||||
|
if (filter is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = new FrostFsFilter(
|
||||||
|
filter.Name,
|
||||||
|
filter.Key,
|
||||||
|
(int)filter.Op,
|
||||||
|
filter.Value,
|
||||||
|
[.. filter.Filters.Select(f => f.ToModel())]);
|
||||||
|
|
||||||
|
return model;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,11 +8,6 @@ public static class ObjectAttributeMapper
|
||||||
{
|
{
|
||||||
public static Header.Types.Attribute ToMessage(this FrostFsAttributePair attribute)
|
public static Header.Types.Attribute ToMessage(this FrostFsAttributePair attribute)
|
||||||
{
|
{
|
||||||
if (attribute is null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(attribute));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Header.Types.Attribute
|
return new Header.Types.Attribute
|
||||||
{
|
{
|
||||||
Key = attribute.Key,
|
Key = attribute.Key,
|
||||||
|
|
|
@ -24,25 +24,14 @@ public static class ObjectHeaderMapper
|
||||||
_ => throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'.")
|
_ => throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'.")
|
||||||
};
|
};
|
||||||
|
|
||||||
FrostFsSplit? split = null;
|
FrostFsSplit? split = header!.Split != null
|
||||||
|
? header.Split.ToModel()
|
||||||
if (header.Split != null)
|
: null;
|
||||||
{
|
|
||||||
var children = header.Split.Children.Count != 0 ? new ReadOnlyCollection<FrostFsObjectId>(
|
|
||||||
header.Split.Children.Select(x => x.ToModel()).ToList()) : null;
|
|
||||||
|
|
||||||
split = new FrostFsSplit(new SplitId(header.Split.SplitId.ToUuid()),
|
|
||||||
header.Split.Previous?.ToModel(),
|
|
||||||
header.Split.Parent?.ToModel(),
|
|
||||||
header.Split.ParentHeader?.ToModel(),
|
|
||||||
null,
|
|
||||||
children);
|
|
||||||
}
|
|
||||||
|
|
||||||
var model = new FrostFsObjectHeader(
|
var model = new FrostFsObjectHeader(
|
||||||
new FrostFsContainerId(Base58.Encode(header.ContainerId.Value.Span)),
|
new FrostFsContainerId(Base58.Encode(header.ContainerId.Value.Span)),
|
||||||
objTypeName,
|
objTypeName,
|
||||||
header.Attributes.Select(attribute => attribute.ToModel()).ToArray(),
|
[.. header.Attributes.Select(attribute => attribute.ToModel())],
|
||||||
split,
|
split,
|
||||||
header.OwnerId.ToModel(),
|
header.OwnerId.ToModel(),
|
||||||
header.Version.ToModel())
|
header.Version.ToModel())
|
||||||
|
@ -52,4 +41,23 @@ public static class ObjectHeaderMapper
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static FrostFsSplit ToModel(this Header.Types.Split split)
|
||||||
|
{
|
||||||
|
if (split is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(split));
|
||||||
|
}
|
||||||
|
|
||||||
|
var children = split!.Children.Count != 0
|
||||||
|
? new ReadOnlyCollection<FrostFsObjectId>([.. split.Children.Select(x => x.ToModel())])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return new FrostFsSplit(new SplitId(split.SplitId.ToUuid()),
|
||||||
|
split.Previous?.ToModel(),
|
||||||
|
split.Parent?.ToModel(),
|
||||||
|
split.ParentHeader?.ToModel(),
|
||||||
|
null,
|
||||||
|
children);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ public static class ObjectIdMapper
|
||||||
|
|
||||||
return new ObjectID
|
return new ObjectID
|
||||||
{
|
{
|
||||||
Value = ByteString.CopyFrom(objectId.ToHash())
|
Value = UnsafeByteOperations.UnsafeWrap(objectId.ToHash())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ public static class OwnerIdMapper
|
||||||
{
|
{
|
||||||
message = new OwnerID
|
message = new OwnerID
|
||||||
{
|
{
|
||||||
Value = ByteString.CopyFrom(model.ToHash())
|
Value = UnsafeByteOperations.UnsafeWrap(model.ToHash())
|
||||||
};
|
};
|
||||||
|
|
||||||
Caches.Owners.Set(model, message, _oneHourExpiration);
|
Caches.Owners.Set(model, message, _oneHourExpiration);
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
using Google.Protobuf;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public static class SessionMapper
|
|
||||||
{
|
|
||||||
public static byte[] Serialize(this Session.SessionToken token)
|
|
||||||
{
|
|
||||||
if (token is null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(token));
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] bytes = new byte[token.CalculateSize()];
|
|
||||||
using CodedOutputStream stream = new(bytes);
|
|
||||||
token.WriteTo(stream);
|
|
||||||
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Session.SessionToken Deserialize(this Session.SessionToken token, byte[] bytes)
|
|
||||||
{
|
|
||||||
token.MergeFrom(bytes);
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -23,9 +23,9 @@ public static class SignatureMapper
|
||||||
|
|
||||||
return new Refs.Signature
|
return new Refs.Signature
|
||||||
{
|
{
|
||||||
Key = ByteString.CopyFrom(signature.Key),
|
Key = UnsafeByteOperations.UnsafeWrap(signature.Key),
|
||||||
Scheme = scheme,
|
Scheme = scheme,
|
||||||
Sign = ByteString.CopyFrom(signature.Sign)
|
Sign = UnsafeByteOperations.UnsafeWrap(signature.Sign)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client.Mappers.GRPC;
|
namespace FrostFS.SDK.Client.Mappers.GRPC;
|
||||||
|
|
||||||
|
@ -13,6 +14,9 @@ public static class StatusMapper
|
||||||
|
|
||||||
return codeName is null
|
return codeName is null
|
||||||
? throw new ArgumentException($"Unknown StatusCode. Value: '{status.Code}'.")
|
? throw new ArgumentException($"Unknown StatusCode. Value: '{status.Code}'.")
|
||||||
: new FrostFsResponseStatus((FrostFsStatusCode)status.Code, status.Message);
|
: new FrostFsResponseStatus(
|
||||||
|
(FrostFsStatusCode)status.Code,
|
||||||
|
status.Message,
|
||||||
|
string.Join(", ", status.Details.Select(d => System.Text.Encoding.UTF8.GetString([.. d.Value]))));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
using Google.Protobuf;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public struct FrostFsChain(byte[] raw) : System.IEquatable<FrostFsChain>
|
|
||||||
{
|
|
||||||
private ByteString? grpcRaw;
|
|
||||||
|
|
||||||
public byte[] Raw { get; } = raw;
|
|
||||||
|
|
||||||
internal ByteString GetRaw()
|
|
||||||
{
|
|
||||||
return grpcRaw ??= ByteString.CopyFrom(Raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override readonly bool Equals(object obj)
|
|
||||||
{
|
|
||||||
var chain = (FrostFsChain)obj;
|
|
||||||
return Equals(chain);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override readonly int GetHashCode()
|
|
||||||
{
|
|
||||||
return Raw.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(FrostFsChain left, FrostFsChain right)
|
|
||||||
{
|
|
||||||
return left.Equals(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(FrostFsChain left, FrostFsChain right)
|
|
||||||
{
|
|
||||||
return !(left == right);
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly bool Equals(FrostFsChain other)
|
|
||||||
{
|
|
||||||
return Raw == other.Raw;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -34,21 +34,18 @@ public class FrostFsContainerId
|
||||||
throw new FrostFsInvalidObjectException();
|
throw new FrostFsInvalidObjectException();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ContainerID ContainerID
|
public ContainerID GetContainerID()
|
||||||
{
|
{
|
||||||
get
|
if (this.containerID != null)
|
||||||
|
return this.containerID;
|
||||||
|
|
||||||
|
if (modelId != null)
|
||||||
{
|
{
|
||||||
if (this.containerID != null)
|
this.containerID = this.ToMessage();
|
||||||
return this.containerID;
|
return this.containerID;
|
||||||
|
|
||||||
if (modelId != null)
|
|
||||||
{
|
|
||||||
this.containerID = this.ToMessage();
|
|
||||||
return this.containerID;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new FrostFsInvalidObjectException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new FrostFsInvalidObjectException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|
|
@ -91,10 +91,13 @@ public class FrostFsContainerInfo
|
||||||
throw new ArgumentNullException("PlacementPolicy is null");
|
throw new ArgumentNullException("PlacementPolicy is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Span<byte> nonce = stackalloc byte[16];
|
||||||
|
Nonce.ToBytes(nonce);
|
||||||
|
|
||||||
this.container = new Container.Container()
|
this.container = new Container.Container()
|
||||||
{
|
{
|
||||||
PlacementPolicy = PlacementPolicy.Value.GetPolicy(),
|
PlacementPolicy = PlacementPolicy.Value.GetPolicy(),
|
||||||
Nonce = ByteString.CopyFrom(Nonce.ToBytes()),
|
Nonce = ByteString.CopyFrom(nonce),
|
||||||
OwnerId = Owner?.OwnerID,
|
OwnerId = Owner?.OwnerID,
|
||||||
Version = Version?.VersionID
|
Version = Version?.VersionID
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ public class CheckSum
|
||||||
|
|
||||||
public static CheckSum CreateCheckSum(byte[] content)
|
public static CheckSum CreateCheckSum(byte[] content)
|
||||||
{
|
{
|
||||||
return new CheckSum { hash = content.Sha256() };
|
return new CheckSum { hash = DataHasher.Sha256(content.AsSpan()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|
28
src/FrostFS.SDK.Client/Models/Netmap/FrostFsFilter.cs
Normal file
28
src/FrostFS.SDK.Client/Models/Netmap/FrostFsFilter.cs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
using System.Linq;
|
||||||
|
using FrostFS.Netmap;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK;
|
||||||
|
|
||||||
|
public class FrostFsFilter(string name, string key, int operation, string value, FrostFsFilter[] filters) : IFrostFsFilter
|
||||||
|
{
|
||||||
|
public string Name { get; } = name;
|
||||||
|
public string Key { get; } = key;
|
||||||
|
public int Operation { get; } = operation;
|
||||||
|
public string Value { get; } = value;
|
||||||
|
public FrostFsFilter[] Filters { get; } = filters;
|
||||||
|
|
||||||
|
internal Filter GetMessage()
|
||||||
|
{
|
||||||
|
var filter = new Filter()
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
Key = Key,
|
||||||
|
Op = (Operation)Operation,
|
||||||
|
Value = Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
filter.Filters.AddRange(Filters.Select(f => f.GetMessage()));
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,10 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
using FrostFS.SDK.Client;
|
||||||
|
using FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
using FrostFS.SDK.Cryptography;
|
||||||
|
|
||||||
namespace FrostFS.SDK;
|
namespace FrostFS.SDK;
|
||||||
|
|
||||||
|
@ -7,4 +13,234 @@ public class FrostFsNetmapSnapshot(ulong epoch, IReadOnlyList<FrostFsNodeInfo> n
|
||||||
public ulong Epoch { get; private set; } = epoch;
|
public ulong Epoch { get; private set; } = epoch;
|
||||||
|
|
||||||
public IReadOnlyList<FrostFsNodeInfo> NodeInfoCollection { get; private set; } = nodeInfoCollection;
|
public IReadOnlyList<FrostFsNodeInfo> NodeInfoCollection { get; private set; } = nodeInfoCollection;
|
||||||
|
|
||||||
|
internal static INormalizer NewReverseMinNorm(double minV)
|
||||||
|
{
|
||||||
|
return new ReverseMinNorm { min = minV };
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSigmoidNorm returns a normalizer which
|
||||||
|
// normalize values in range of 0.0 to 1.0 to a scaled sigmoid.
|
||||||
|
internal static INormalizer NewSigmoidNorm(double scale)
|
||||||
|
{
|
||||||
|
return new SigmoidNorm(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlacementVectors sorts container nodes returned by ContainerNodes method
|
||||||
|
// and returns placement vectors for the entity identified by the given pivot.
|
||||||
|
// For example, in order to build node list to store the object, binary-encoded
|
||||||
|
// object identifier can be used as pivot. Result is deterministic for
|
||||||
|
// the fixed NetMap and parameters.
|
||||||
|
public FrostFsNodeInfo[][] PlacementVectors(FrostFsNodeInfo[][] vectors, byte[] pivot)
|
||||||
|
{
|
||||||
|
if (vectors is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(vectors));
|
||||||
|
}
|
||||||
|
|
||||||
|
using var murmur3 = new Murmur3(0);
|
||||||
|
var hash = murmur3.GetCheckSum64(pivot);
|
||||||
|
|
||||||
|
var wf = Tools.DefaultWeightFunc(NodeInfoCollection.ToArray());
|
||||||
|
|
||||||
|
var result = new FrostFsNodeInfo[vectors.Length][];
|
||||||
|
var maxSize = vectors.Max(x => x.Length);
|
||||||
|
|
||||||
|
var spanWeigths = new double[maxSize];
|
||||||
|
|
||||||
|
for (int i = 0; i < vectors.Length; i++)
|
||||||
|
{
|
||||||
|
result[i] = new FrostFsNodeInfo[vectors[i].Length];
|
||||||
|
|
||||||
|
for (int j = 0; j < vectors[i].Length; j++)
|
||||||
|
{
|
||||||
|
result[i][j] = vectors[i][j];
|
||||||
|
}
|
||||||
|
|
||||||
|
Tools.AppendWeightsTo(result[i], wf, ref spanWeigths);
|
||||||
|
|
||||||
|
result[i] = Tools.SortHasherSliceByWeightValue(result[i].ToList<FrostFsNodeInfo>(), spanWeigths, hash).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectFilterNodes returns a two-dimensional list of nodes as a result of applying the
|
||||||
|
// given SelectFilterExpr to the NetMap.
|
||||||
|
// If the SelectFilterExpr contains only filters, the result contains a single row with the
|
||||||
|
// result of the last filter application.
|
||||||
|
// If the SelectFilterExpr contains only selectors, the result contains the selection rows
|
||||||
|
// of the last select application.
|
||||||
|
List<List<FrostFsNodeInfo>> SelectFilterNodes(SelectFilterExpr expr)
|
||||||
|
{
|
||||||
|
var policy = new FrostFsPlacementPolicy(false, expr.Cbf, [expr.Selector], expr.Filters);
|
||||||
|
|
||||||
|
var ctx = new Context(this)
|
||||||
|
{
|
||||||
|
Cbf = expr.Cbf
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ProcessFilters(policy);
|
||||||
|
ctx.ProcessSelectors(policy);
|
||||||
|
|
||||||
|
var ret = new List<List<FrostFsNodeInfo>>();
|
||||||
|
|
||||||
|
if (expr.Selector == null)
|
||||||
|
{
|
||||||
|
var lastFilter = expr.Filters.Last();
|
||||||
|
|
||||||
|
var subCollestion = new List<FrostFsNodeInfo>();
|
||||||
|
ret.Add(subCollestion);
|
||||||
|
|
||||||
|
foreach (var nodeInfo in NodeInfoCollection)
|
||||||
|
{
|
||||||
|
if (ctx.Match(ctx.ProcessedFilters[lastFilter.Name], nodeInfo))
|
||||||
|
{
|
||||||
|
subCollestion.Add(nodeInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (expr.Selector.Name != null)
|
||||||
|
{
|
||||||
|
var sel = ctx.GetSelection(ctx.ProcessedSelectors[expr.Selector.Name]);
|
||||||
|
|
||||||
|
foreach (var ns in sel)
|
||||||
|
{
|
||||||
|
var subCollestion = new List<FrostFsNodeInfo>();
|
||||||
|
ret.Add(subCollestion);
|
||||||
|
foreach (var n in ns)
|
||||||
|
{
|
||||||
|
subCollestion.Add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Func<FrostFsNodeInfo, double> NewWeightFunc(INormalizer capNorm, INormalizer priceNorm)
|
||||||
|
{
|
||||||
|
return new Func<FrostFsNodeInfo, double>((FrostFsNodeInfo nodeInfo) =>
|
||||||
|
{
|
||||||
|
return capNorm.Normalize(nodeInfo.GetCapacity()) * priceNorm.Normalize(nodeInfo.Price);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FrostFsNodeInfo[] FlattenNodes(List<List<FrostFsNodeInfo>> nodes)
|
||||||
|
{
|
||||||
|
int sz = 0;
|
||||||
|
foreach (var ns in nodes)
|
||||||
|
{
|
||||||
|
sz += ns.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new FrostFsNodeInfo[sz];
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var ns in nodes)
|
||||||
|
{
|
||||||
|
foreach (var n in ns)
|
||||||
|
{
|
||||||
|
result[i++] = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerNodes returns two-dimensional list of nodes as a result of applying
|
||||||
|
// given PlacementPolicy to the NetMap. Each line of the list corresponds to a
|
||||||
|
// replica descriptor. Line order corresponds to order of ReplicaDescriptor list
|
||||||
|
// in the policy. Nodes are pre-filtered according to the Filter list from
|
||||||
|
// the policy, and then selected by Selector list. Result is deterministic for
|
||||||
|
// the fixed NetMap and parameters.
|
||||||
|
//
|
||||||
|
// Result can be used in PlacementVectors.
|
||||||
|
public FrostFsNodeInfo[][] ContainerNodes(FrostFsPlacementPolicy p, byte[]? pivot)
|
||||||
|
{
|
||||||
|
var c = new Context(this)
|
||||||
|
{
|
||||||
|
Cbf = p.BackupFactor == 0 ? 3 : p.BackupFactor
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pivot != null && pivot.Length > 0)
|
||||||
|
{
|
||||||
|
c.HrwSeed = pivot;
|
||||||
|
|
||||||
|
using var murmur = new Murmur3(0);
|
||||||
|
c.HrwSeedHash = murmur.GetCheckSum64(pivot);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ProcessFilters(p);
|
||||||
|
c.ProcessSelectors(p);
|
||||||
|
|
||||||
|
var unique = p.IsUnique();
|
||||||
|
|
||||||
|
var result = new List<List<FrostFsNodeInfo>>(p.Replicas.Length);
|
||||||
|
for (int i = 0; i < p.Replicas.Length; i++)
|
||||||
|
{
|
||||||
|
result.Add([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that the cached selectors are not used when the policy contains the UNIQUE flag.
|
||||||
|
// This is necessary because each selection vector affects potentially the subsequent vectors
|
||||||
|
// and thus we call getSelection in such case, in order to take into account nodes previously
|
||||||
|
// marked as used by earlier replicas.
|
||||||
|
for (int i = 0; i < p.Replicas.Length; i++)
|
||||||
|
{
|
||||||
|
var sName = p.Replicas[i].Selector;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(sName) && !(p.Replicas.Length == 1 && p.Selectors.Count == 1))
|
||||||
|
{
|
||||||
|
var s = new FrostFsSelector(string.Empty)
|
||||||
|
{
|
||||||
|
Count = p.Replicas[i].CountNodes(),
|
||||||
|
Filter = Context.mainFilterName
|
||||||
|
};
|
||||||
|
|
||||||
|
var nodes = c.GetSelection(s);
|
||||||
|
result[i].AddRange(FlattenNodes(nodes));
|
||||||
|
|
||||||
|
if (unique)
|
||||||
|
{
|
||||||
|
foreach (var n in result[i])
|
||||||
|
{
|
||||||
|
c.UsedNodes[n.Hash()] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unique)
|
||||||
|
{
|
||||||
|
if (!c.ProcessedSelectors.TryGetValue(sName, out var s) || s == null)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"selector not found: {sName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodes = c.GetSelection(c.ProcessedSelectors[sName]);
|
||||||
|
|
||||||
|
result[i].AddRange(FlattenNodes(nodes));
|
||||||
|
|
||||||
|
foreach (var n in result[i])
|
||||||
|
{
|
||||||
|
c.UsedNodes[n.Hash()] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var nodes = c.Selections[sName];
|
||||||
|
result[i].AddRange(FlattenNodes(nodes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var collection = new FrostFsNodeInfo[result.Count][];
|
||||||
|
for (int i = 0; i < result.Count; i++)
|
||||||
|
{
|
||||||
|
collection[i] = [.. result[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
using FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
using FrostFS.SDK.Cryptography;
|
||||||
|
|
||||||
namespace FrostFS.SDK;
|
namespace FrostFS.SDK;
|
||||||
|
|
||||||
|
@ -8,11 +12,69 @@ public class FrostFsNodeInfo(
|
||||||
NodeState state,
|
NodeState state,
|
||||||
IReadOnlyCollection<string> addresses,
|
IReadOnlyCollection<string> addresses,
|
||||||
IReadOnlyDictionary<string, string> attributes,
|
IReadOnlyDictionary<string, string> attributes,
|
||||||
ReadOnlyMemory<byte> publicKey)
|
ReadOnlyMemory<byte> publicKey) : IHasher
|
||||||
{
|
{
|
||||||
public NodeState State { get; private set; } = state;
|
private ulong _hash;
|
||||||
public FrostFsVersion Version { get; private set; } = version;
|
|
||||||
public IReadOnlyCollection<string> Addresses { get; private set; } = addresses;
|
// attrPrice is a key to the node attribute that indicates the
|
||||||
public IReadOnlyDictionary<string, string> Attributes { get; private set; } = attributes;
|
// price in GAS tokens for storing one GB of data during one Epoch.
|
||||||
public ReadOnlyMemory<byte> PublicKey { get; private set; } = publicKey;
|
internal const string AttrPrice = "Price";
|
||||||
|
|
||||||
|
// attrCapacity is a key to the node attribute that indicates the
|
||||||
|
// total available disk space in Gigabytes.
|
||||||
|
internal const string AttrCapacity = "Capacity";
|
||||||
|
|
||||||
|
// attrExternalAddr is a key for the attribute storing node external addresses.
|
||||||
|
internal const string AttrExternalAddr = "ExternalAddr";
|
||||||
|
|
||||||
|
// sepExternalAddr is a separator for multi-value ExternalAddr attribute.
|
||||||
|
internal const string SepExternalAddr = ",";
|
||||||
|
|
||||||
|
private ulong price = ulong.MaxValue;
|
||||||
|
|
||||||
|
public NodeState State { get; } = state;
|
||||||
|
|
||||||
|
public FrostFsVersion Version { get; } = version;
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> Addresses { get; } = addresses;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string> Attributes { get; } = attributes;
|
||||||
|
|
||||||
|
public ReadOnlyMemory<byte> PublicKey { get; } = publicKey;
|
||||||
|
|
||||||
|
public ulong Hash()
|
||||||
|
{
|
||||||
|
if (_hash == 0)
|
||||||
|
{
|
||||||
|
using var murmur3 = new Murmur3(0);
|
||||||
|
murmur3.Initialize();
|
||||||
|
_hash = murmur3.GetCheckSum64(PublicKey.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
return _hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ulong GetCapacity()
|
||||||
|
{
|
||||||
|
if (!Attributes.TryGetValue(AttrCapacity, out var val))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return ulong.Parse(val, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ulong Price
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (price == ulong.MaxValue)
|
||||||
|
{
|
||||||
|
if (!Attributes.TryGetValue(AttrPrice, out var val))
|
||||||
|
price = 0;
|
||||||
|
else
|
||||||
|
price = uint.Parse(val, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
using FrostFS.Netmap;
|
using FrostFS.Netmap;
|
||||||
|
@ -7,18 +7,31 @@ using FrostFS.SDK.Client;
|
||||||
|
|
||||||
namespace FrostFS.SDK;
|
namespace FrostFS.SDK;
|
||||||
|
|
||||||
public struct FrostFsPlacementPolicy(bool unique, params FrostFsReplica[] replicas)
|
public struct FrostFsPlacementPolicy(bool unique,
|
||||||
|
uint backupFactor,
|
||||||
|
Collection<FrostFsSelector> selectors,
|
||||||
|
Collection<FrostFsFilter> filters,
|
||||||
|
params FrostFsReplica[] replicas)
|
||||||
: IEquatable<FrostFsPlacementPolicy>
|
: IEquatable<FrostFsPlacementPolicy>
|
||||||
{
|
{
|
||||||
private PlacementPolicy policy;
|
private PlacementPolicy? policy;
|
||||||
|
|
||||||
public FrostFsReplica[] Replicas { get; private set; } = replicas;
|
public FrostFsReplica[] Replicas { get; } = replicas;
|
||||||
public bool Unique { get; private set; } = unique;
|
|
||||||
|
public Collection<FrostFsSelector> Selectors { get; } = selectors;
|
||||||
|
|
||||||
|
public Collection<FrostFsFilter> Filters { get; } = filters;
|
||||||
|
|
||||||
|
public bool Unique { get; } = unique;
|
||||||
|
|
||||||
|
public uint BackupFactor { get; } = backupFactor;
|
||||||
|
|
||||||
public override readonly bool Equals(object obj)
|
public override readonly bool Equals(object obj)
|
||||||
{
|
{
|
||||||
if (obj is null)
|
if (obj is null)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var other = (FrostFsPlacementPolicy)obj;
|
var other = (FrostFsPlacementPolicy)obj;
|
||||||
|
|
||||||
|
@ -34,9 +47,20 @@ public struct FrostFsPlacementPolicy(bool unique, params FrostFsReplica[] replic
|
||||||
Filters = { },
|
Filters = { },
|
||||||
Selectors = { },
|
Selectors = { },
|
||||||
Replicas = { },
|
Replicas = { },
|
||||||
Unique = Unique
|
Unique = Unique,
|
||||||
|
ContainerBackupFactor = BackupFactor
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (Selectors != null && Selectors.Count > 0)
|
||||||
|
{
|
||||||
|
policy.Selectors.AddRange(Selectors.Select(s => s.GetMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Filters != null && Filters.Count > 0)
|
||||||
|
{
|
||||||
|
policy.Filters.AddRange(Filters.Select(s => s.ToMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var replica in Replicas)
|
foreach (var replica in Replicas)
|
||||||
{
|
{
|
||||||
policy.Replicas.Add(replica.ToMessage());
|
policy.Replicas.Add(replica.ToMessage());
|
||||||
|
@ -46,14 +70,10 @@ public struct FrostFsPlacementPolicy(bool unique, params FrostFsReplica[] replic
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
//public static FrostFsPlacementPolicy ToModel(placementPolicy)
|
internal readonly bool IsUnique()
|
||||||
//{
|
{
|
||||||
// return new FrostFsPlacementPolicy(
|
return Unique || Replicas.Any(r => r.EcDataCount != 0 || r.EcParityCount != 0);
|
||||||
// placementPolicy.Unique,
|
}
|
||||||
// placementPolicy.Replicas.Select(replica => replica.ToModel()).ToArray()
|
|
||||||
// );
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
public override readonly int GetHashCode()
|
public override readonly int GetHashCode()
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,6 +6,8 @@ public struct FrostFsReplica : IEquatable<FrostFsReplica>
|
||||||
{
|
{
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
public string Selector { get; set; }
|
public string Selector { get; set; }
|
||||||
|
public uint EcDataCount { get; set; }
|
||||||
|
public uint EcParityCount { get; set; }
|
||||||
|
|
||||||
public FrostFsReplica(int count, string? selector = null)
|
public FrostFsReplica(int count, string? selector = null)
|
||||||
{
|
{
|
||||||
|
@ -18,16 +20,23 @@ public struct FrostFsReplica : IEquatable<FrostFsReplica>
|
||||||
public override readonly bool Equals(object obj)
|
public override readonly bool Equals(object obj)
|
||||||
{
|
{
|
||||||
if (obj is null)
|
if (obj is null)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var other = (FrostFsReplica)obj;
|
var other = (FrostFsReplica)obj;
|
||||||
|
|
||||||
return Count == other.Count && Selector == other.Selector;
|
return Count == other.Count && Selector == other.Selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly uint CountNodes()
|
||||||
|
{
|
||||||
|
return Count != 0 ? (uint)Count : EcDataCount + EcParityCount;
|
||||||
|
}
|
||||||
|
|
||||||
public override readonly int GetHashCode()
|
public override readonly int GetHashCode()
|
||||||
{
|
{
|
||||||
return Count + Selector.GetHashCode();
|
return (Count + Selector.GetHashCode()) ^ (int)EcDataCount ^ (int)EcParityCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool operator ==(FrostFsReplica left, FrostFsReplica right)
|
public static bool operator ==(FrostFsReplica left, FrostFsReplica right)
|
||||||
|
@ -42,6 +51,9 @@ public struct FrostFsReplica : IEquatable<FrostFsReplica>
|
||||||
|
|
||||||
public readonly bool Equals(FrostFsReplica other)
|
public readonly bool Equals(FrostFsReplica other)
|
||||||
{
|
{
|
||||||
return Count == other.Count && Selector == other.Selector;
|
return Count == other.Count
|
||||||
|
&& Selector == other.Selector
|
||||||
|
&& EcDataCount == other.EcDataCount
|
||||||
|
&& EcParityCount == other.EcParityCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
24
src/FrostFS.SDK.Client/Models/Netmap/FrostFsSelector.cs
Normal file
24
src/FrostFS.SDK.Client/Models/Netmap/FrostFsSelector.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using FrostFS.Netmap;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK;
|
||||||
|
|
||||||
|
public class FrostFsSelector(string name)
|
||||||
|
{
|
||||||
|
public string Name { get; } = name;
|
||||||
|
public uint Count { get; set; }
|
||||||
|
public int Clause { get; set; }
|
||||||
|
public string? Attribute { get; set; }
|
||||||
|
public string? Filter { get; set; }
|
||||||
|
|
||||||
|
internal Selector GetMessage()
|
||||||
|
{
|
||||||
|
return new Selector()
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
Clause = (Clause)Clause,
|
||||||
|
Count = Count,
|
||||||
|
Filter = Filter ?? string.Empty,
|
||||||
|
Attribute = Attribute ?? string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
11
src/FrostFS.SDK.Client/Models/Netmap/IFrostFsFilter.cs
Normal file
11
src/FrostFS.SDK.Client/Models/Netmap/IFrostFsFilter.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
namespace FrostFS.SDK
|
||||||
|
{
|
||||||
|
public interface IFrostFsFilter
|
||||||
|
{
|
||||||
|
FrostFsFilter[] Filters { get; }
|
||||||
|
string Key { get; }
|
||||||
|
string Name { get; }
|
||||||
|
int Operation { get; }
|
||||||
|
string Value { get; }
|
||||||
|
}
|
||||||
|
}
|
7
src/FrostFS.SDK.Client/Models/Netmap/NodeAttrPair.cs
Normal file
7
src/FrostFS.SDK.Client/Models/Netmap/NodeAttrPair.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace FrostFS.SDK;
|
||||||
|
|
||||||
|
struct NodeAttrPair
|
||||||
|
{
|
||||||
|
internal string attr;
|
||||||
|
internal FrostFsNodeInfo[] nodes;
|
||||||
|
}
|
8
src/FrostFS.SDK.Client/Models/Netmap/Placement/Clause.cs
Normal file
8
src/FrostFS.SDK.Client/Models/Netmap/Placement/Clause.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
public enum FrostFsClause
|
||||||
|
{
|
||||||
|
Unspecified = 0,
|
||||||
|
Same,
|
||||||
|
Distinct
|
||||||
|
}
|
456
src/FrostFS.SDK.Client/Models/Netmap/Placement/Context.cs
Normal file
456
src/FrostFS.SDK.Client/Models/Netmap/Placement/Context.cs
Normal file
|
@ -0,0 +1,456 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
internal struct Context
|
||||||
|
{
|
||||||
|
private const string errInvalidFilterName = "filter name is invalid";
|
||||||
|
private const string errInvalidFilterOp = "invalid filter operation";
|
||||||
|
private const string errFilterNotFound = "filter not found";
|
||||||
|
private const string errNonEmptyFilters = "simple filter contains sub-filters";
|
||||||
|
private const string errNotEnoughNodes = "not enough nodes to SELECT from";
|
||||||
|
private const string errUnnamedTopFilter = "unnamed top-level filter";
|
||||||
|
|
||||||
|
internal const string mainFilterName = "*";
|
||||||
|
internal const string likeWildcard = "*";
|
||||||
|
|
||||||
|
// network map to operate on
|
||||||
|
internal FrostFsNetmapSnapshot NetMap { get; }
|
||||||
|
|
||||||
|
// cache of processed filters
|
||||||
|
internal Dictionary<string, FrostFsFilter> ProcessedFilters { get; } = [];
|
||||||
|
|
||||||
|
// cache of processed selectors
|
||||||
|
internal Dictionary<string, FrostFsSelector> ProcessedSelectors { get; } = [];
|
||||||
|
|
||||||
|
// stores results of selector processing
|
||||||
|
internal Dictionary<string, List<List<FrostFsNodeInfo>>> Selections { get; } = [];
|
||||||
|
|
||||||
|
// cache of parsed numeric values
|
||||||
|
internal Dictionary<string, ulong> NumCache { get; } = [];
|
||||||
|
|
||||||
|
internal byte[]? HrwSeed { get; set; }
|
||||||
|
|
||||||
|
// hrw.Hash of hrwSeed
|
||||||
|
internal ulong HrwSeedHash { get; set; }
|
||||||
|
|
||||||
|
// container backup factor
|
||||||
|
internal uint Cbf { get; set; }
|
||||||
|
|
||||||
|
// nodes already used in previous selections, which is needed when the placement
|
||||||
|
// policy uses the UNIQUE flag. Nodes marked as used are not used in subsequent
|
||||||
|
// base selections.
|
||||||
|
internal Dictionary<ulong, bool> UsedNodes { get; } = [];
|
||||||
|
|
||||||
|
// If true, returns an error when netmap does not contain enough nodes for selection.
|
||||||
|
// By default best effort is taken.
|
||||||
|
internal bool Strict { get; set; }
|
||||||
|
|
||||||
|
// weightFunc is a weighting function for determining node priority
|
||||||
|
// which combines low price and high performance
|
||||||
|
private readonly Func<FrostFsNodeInfo, double> weightFunc;
|
||||||
|
|
||||||
|
public Context(FrostFsNetmapSnapshot netMap)
|
||||||
|
{
|
||||||
|
NetMap = netMap;
|
||||||
|
weightFunc = Tools.DefaultWeightFunc(NetMap.NodeInfoCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ProcessFilters(FrostFsPlacementPolicy policy)
|
||||||
|
{
|
||||||
|
foreach (var filter in policy.Filters)
|
||||||
|
{
|
||||||
|
ProcessFilter(filter, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly void ProcessFilter(FrostFsFilter filter, bool top)
|
||||||
|
{
|
||||||
|
var filterName = filter.Name;
|
||||||
|
if (filterName == mainFilterName)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"{errInvalidFilterName}: '{errInvalidFilterName}' is reserved");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top && string.IsNullOrEmpty(filterName))
|
||||||
|
{
|
||||||
|
throw new FrostFsException(errUnnamedTopFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!top && !string.IsNullOrEmpty(filterName) && !ProcessedFilters.ContainsKey(filterName))
|
||||||
|
{
|
||||||
|
throw new FrostFsException(errFilterNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.Operation == (int)Operation.AND ||
|
||||||
|
filter.Operation == (int)Operation.OR ||
|
||||||
|
filter.Operation == (int)Operation.NOT)
|
||||||
|
{
|
||||||
|
foreach (var f in filter.Filters)
|
||||||
|
ProcessFilter(f, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (filter.Filters.Length != 0)
|
||||||
|
{
|
||||||
|
throw new FrostFsException(errNonEmptyFilters);
|
||||||
|
}
|
||||||
|
else if (!top && !string.IsNullOrEmpty(filterName))
|
||||||
|
{
|
||||||
|
// named reference
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (filter.Operation)
|
||||||
|
{
|
||||||
|
case (int)Operation.EQ:
|
||||||
|
case (int)Operation.NE:
|
||||||
|
case (int)Operation.LIKE:
|
||||||
|
break;
|
||||||
|
case (int)Operation.GT:
|
||||||
|
case (int)Operation.GE:
|
||||||
|
case (int)Operation.LT:
|
||||||
|
case (int)Operation.LE:
|
||||||
|
{
|
||||||
|
var n = uint.Parse(filter.Value, CultureInfo.InvariantCulture);
|
||||||
|
NumCache[filter.Value] = n;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new FrostFsException($"{errInvalidFilterOp}: {filter.Operation}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top)
|
||||||
|
{
|
||||||
|
ProcessedFilters[filterName] = filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processSelectors processes selectors and returns error is any of them is invalid.
|
||||||
|
internal void ProcessSelectors(FrostFsPlacementPolicy policy)
|
||||||
|
{
|
||||||
|
foreach (var selector in policy.Selectors)
|
||||||
|
{
|
||||||
|
var filterName = selector.Filter;
|
||||||
|
if (filterName != mainFilterName)
|
||||||
|
{
|
||||||
|
if (selector.Filter == null || !ProcessedFilters.ContainsKey(selector.Filter))
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"{errFilterNotFound}: SELECT FROM '{filterName}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessedSelectors[selector.Name] = selector;
|
||||||
|
|
||||||
|
var selection = GetSelection(selector);
|
||||||
|
|
||||||
|
Selections[selector.Name] = selection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcNodesCount returns number of buckets and minimum number of nodes in every bucket
|
||||||
|
// for the given selector.
|
||||||
|
static (int bucketCount, int nodesInBucket) CalcNodesCount(FrostFsSelector selector)
|
||||||
|
{
|
||||||
|
return selector.Clause == (int)FrostFsClause.Same
|
||||||
|
? (1, (int)selector.Count)
|
||||||
|
: ((int)selector.Count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSelectionBase returns nodes grouped by selector attribute.
|
||||||
|
// It it guaranteed that each pair will contain at least one node.
|
||||||
|
internal NodeAttrPair[] GetSelectionBase(FrostFsSelector selector)
|
||||||
|
{
|
||||||
|
var fName = selector.Filter ?? throw new FrostFsException("Filter name for selector is empty");
|
||||||
|
|
||||||
|
_ = ProcessedFilters.TryGetValue(fName, out var f);
|
||||||
|
|
||||||
|
var isMain = fName == mainFilterName;
|
||||||
|
var result = new List<NodeAttrPair>();
|
||||||
|
|
||||||
|
var nodeMap = new Dictionary<string, List<FrostFsNodeInfo>>();
|
||||||
|
var attr = selector.Attribute;
|
||||||
|
|
||||||
|
foreach (var node in NetMap.NodeInfoCollection)
|
||||||
|
{
|
||||||
|
if (UsedNodes.ContainsKey(node.Hash()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMain || Match(f, node))
|
||||||
|
{
|
||||||
|
if (attr == null)
|
||||||
|
{
|
||||||
|
// Default attribute is transparent identifier which is different for every node.
|
||||||
|
result.Add(new NodeAttrPair { attr = "", nodes = [node] });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var v = node.Attributes[attr];
|
||||||
|
if (!nodeMap.TryGetValue(v, out var nodes) || nodes == null)
|
||||||
|
{
|
||||||
|
nodeMap[v] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeMap[v].Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(attr))
|
||||||
|
{
|
||||||
|
foreach (var v in nodeMap)
|
||||||
|
{
|
||||||
|
result.Add(new NodeAttrPair() { attr = v.Key, nodes = [.. v.Value] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HrwSeed != null && HrwSeed.Length != 0)
|
||||||
|
{
|
||||||
|
double[] ws = [];
|
||||||
|
|
||||||
|
var sortedNodes = new NodeAttrPair[result.Count];
|
||||||
|
|
||||||
|
for (int i = 0; i < result.Count; i++)
|
||||||
|
{
|
||||||
|
var res = result[i];
|
||||||
|
Tools.AppendWeightsTo(res.nodes, weightFunc, ref ws);
|
||||||
|
sortedNodes[i].nodes = Tools.SortHasherSliceByWeightValue(res.nodes.ToList(), ws, HrwSeedHash).ToArray();
|
||||||
|
sortedNodes[i].attr = result[i].attr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedNodes;
|
||||||
|
}
|
||||||
|
return [.. result];
|
||||||
|
}
|
||||||
|
|
||||||
|
static double CalcBucketWeight(List<FrostFsNodeInfo> ns, MeanIQRAgg a, Func<FrostFsNodeInfo, double> wf)
|
||||||
|
{
|
||||||
|
foreach (var node in ns)
|
||||||
|
{
|
||||||
|
a.Add(wf(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.Compute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSelection returns nodes grouped by s.attribute.
|
||||||
|
// Last argument specifies if more buckets can be used to fulfill CBF.
|
||||||
|
internal List<List<FrostFsNodeInfo>> GetSelection(FrostFsSelector s)
|
||||||
|
{
|
||||||
|
var (bucketCount, nodesInBucket) = CalcNodesCount(s);
|
||||||
|
|
||||||
|
var buckets = GetSelectionBase(s);
|
||||||
|
|
||||||
|
if (Strict && buckets.Length < bucketCount)
|
||||||
|
throw new FrostFsException($"errNotEnoughNodes: '{s.Name}'");
|
||||||
|
|
||||||
|
// We need deterministic output in case there is no pivot.
|
||||||
|
// If pivot is set, buckets are sorted by HRW.
|
||||||
|
// However, because initial order influences HRW order for buckets with equal weights,
|
||||||
|
// we also need to have deterministic input to HRW sorting routine.
|
||||||
|
if (HrwSeed == null || HrwSeed.Length == 0)
|
||||||
|
{
|
||||||
|
buckets = string.IsNullOrEmpty(s.Attribute)
|
||||||
|
? [.. buckets.OrderBy(b => b.nodes[0].Hash())]
|
||||||
|
: [.. buckets.OrderBy(b => b.attr)];
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxNodesInBucket = nodesInBucket * (int)Cbf;
|
||||||
|
|
||||||
|
var res = new List<List<FrostFsNodeInfo>>(buckets.Length);
|
||||||
|
var fallback = new List<List<FrostFsNodeInfo>>(buckets.Length);
|
||||||
|
|
||||||
|
for (int i = 0; i < buckets.Length; i++)
|
||||||
|
{
|
||||||
|
var ns = buckets[i].nodes;
|
||||||
|
if (ns.Length >= maxNodesInBucket)
|
||||||
|
{
|
||||||
|
res.Add(ns.Take(maxNodesInBucket).ToList());
|
||||||
|
}
|
||||||
|
else if (ns.Length >= nodesInBucket)
|
||||||
|
{
|
||||||
|
fallback.Add(new List<FrostFsNodeInfo>(ns));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.Count < bucketCount)
|
||||||
|
{
|
||||||
|
// Fallback to using minimum allowed backup factor (1).
|
||||||
|
res.AddRange(fallback);
|
||||||
|
|
||||||
|
if (Strict && res.Count < bucketCount)
|
||||||
|
{
|
||||||
|
throw new FrostFsException($"{errNotEnoughNodes}: {s}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HrwSeed != null && HrwSeed.Length != 0)
|
||||||
|
{
|
||||||
|
var weights = new double[res.Count];
|
||||||
|
var a = new MeanIQRAgg();
|
||||||
|
|
||||||
|
for (int i = 0; i < res.Count; i++)
|
||||||
|
{
|
||||||
|
a.Clear();
|
||||||
|
weights[i] = CalcBucketWeight(res[i], a, weightFunc);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashers = res.Select(r => new HasherList(r)).ToList();
|
||||||
|
hashers = Tools.SortHasherSliceByWeightValue(hashers, weights, HrwSeedHash);
|
||||||
|
|
||||||
|
for (int i = 0; i < res.Count; i++)
|
||||||
|
{
|
||||||
|
res[i] = hashers[i].Nodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.Count < bucketCount)
|
||||||
|
{
|
||||||
|
if (Strict && res.Count == 0)
|
||||||
|
{
|
||||||
|
throw new FrostFsException(errNotEnoughNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketCount = res.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(s.Attribute))
|
||||||
|
{
|
||||||
|
fallback = res.Skip(bucketCount).ToList();
|
||||||
|
res = res.Take(bucketCount).ToList();
|
||||||
|
|
||||||
|
for (int i = 0; i < fallback.Count; i++)
|
||||||
|
{
|
||||||
|
var index = i % bucketCount;
|
||||||
|
if (res[index].Count >= maxNodesInBucket)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
res[index].AddRange(fallback[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Take(bucketCount).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool MatchKeyValue(FrostFsFilter f, FrostFsNodeInfo nodeInfo)
|
||||||
|
{
|
||||||
|
switch (f.Operation)
|
||||||
|
{
|
||||||
|
case (int)Operation.EQ:
|
||||||
|
return nodeInfo.Attributes.TryGetValue(f.Key, out var val) && val == f.Value;
|
||||||
|
case (int)Operation.LIKE:
|
||||||
|
{
|
||||||
|
var hasPrefix = f.Value.StartsWith(likeWildcard, StringComparison.Ordinal);
|
||||||
|
var hasSuffix = f.Value.EndsWith(likeWildcard, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
var start = hasPrefix ? likeWildcard.Length : 0;
|
||||||
|
var end = hasSuffix ? f.Value.Length - likeWildcard.Length : f.Value.Length;
|
||||||
|
var str = f.Value.Substring(start, end - start);
|
||||||
|
|
||||||
|
if (hasPrefix && hasSuffix)
|
||||||
|
return nodeInfo.Attributes[f.Key].Contains(str);
|
||||||
|
|
||||||
|
if (hasPrefix && !hasSuffix)
|
||||||
|
return nodeInfo.Attributes[f.Key].EndsWith(str, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (!hasPrefix && hasSuffix)
|
||||||
|
return nodeInfo.Attributes[f.Key].StartsWith(str, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
|
||||||
|
return nodeInfo.Attributes[f.Key] == f.Value;
|
||||||
|
}
|
||||||
|
case (int)Operation.NE:
|
||||||
|
return nodeInfo.Attributes[f.Key] != f.Value;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
ulong attr;
|
||||||
|
switch (f.Key)
|
||||||
|
{
|
||||||
|
case FrostFsNodeInfo.AttrPrice:
|
||||||
|
attr = nodeInfo.Price;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FrostFsNodeInfo.AttrCapacity:
|
||||||
|
attr = nodeInfo.GetCapacity();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (!ulong.TryParse(nodeInfo.Attributes[f.Key], NumberStyles.Integer, CultureInfo.InvariantCulture, out attr))
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (f.Operation)
|
||||||
|
{
|
||||||
|
case (int)Operation.GT:
|
||||||
|
return attr > NumCache[f.Value];
|
||||||
|
case (int)Operation.GE:
|
||||||
|
return attr >= NumCache[f.Value];
|
||||||
|
case (int)Operation.LT:
|
||||||
|
return attr < NumCache[f.Value];
|
||||||
|
case (int)Operation.LE:
|
||||||
|
return attr <= NumCache[f.Value];
|
||||||
|
default:
|
||||||
|
// do nothing and return false
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// will not happen if context was created from f (maybe panic?)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// match matches f against b. It returns no errors because
|
||||||
|
// filter should have been parsed during context creation
|
||||||
|
// and missing node properties are considered as a regular fail.
|
||||||
|
internal bool Match(FrostFsFilter f, FrostFsNodeInfo nodeInfo)
|
||||||
|
{
|
||||||
|
switch (f.Operation)
|
||||||
|
{
|
||||||
|
case (int)Operation.NOT:
|
||||||
|
{
|
||||||
|
var inner = f.Filters;
|
||||||
|
var fSub = inner[0];
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(inner[0].Name))
|
||||||
|
{
|
||||||
|
fSub = ProcessedFilters[inner[0].Name];
|
||||||
|
}
|
||||||
|
return !Match(fSub, nodeInfo);
|
||||||
|
}
|
||||||
|
case (int)Operation.AND:
|
||||||
|
case (int)Operation.OR:
|
||||||
|
{
|
||||||
|
for (int i = 0; i < f.Filters.Length; i++)
|
||||||
|
{
|
||||||
|
var fSub = f.Filters[i];
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(f.Filters[i].Name))
|
||||||
|
{
|
||||||
|
fSub = ProcessedFilters[f.Filters[i].Name];
|
||||||
|
}
|
||||||
|
|
||||||
|
var ok = Match(fSub, nodeInfo);
|
||||||
|
|
||||||
|
if (ok == (f.Operation == (int)Operation.OR))
|
||||||
|
{
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Operation == (int)Operation.AND;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return MatchKeyValue(f, nodeInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/FrostFS.SDK.Client/Models/Netmap/Placement/HasherList.cs
Normal file
26
src/FrostFS.SDK.Client/Models/Netmap/Placement/HasherList.cs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
internal sealed class HasherList : IHasher
|
||||||
|
{
|
||||||
|
private readonly List<FrostFsNodeInfo> _nodes;
|
||||||
|
|
||||||
|
internal HasherList(List<FrostFsNodeInfo> nodes)
|
||||||
|
{
|
||||||
|
_nodes = nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal List<FrostFsNodeInfo> Nodes
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _nodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Hash()
|
||||||
|
{
|
||||||
|
return _nodes.Count > 0 ? _nodes[0].Hash() : 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
internal interface IAggregator
|
||||||
|
{
|
||||||
|
void Add(double d);
|
||||||
|
|
||||||
|
double Compute();
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
internal interface IHasher
|
||||||
|
{
|
||||||
|
ulong Hash();
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
interface INormalizer
|
||||||
|
{
|
||||||
|
double Normalize(double w);
|
||||||
|
}
|
20
src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanAgg.cs
Normal file
20
src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanAgg.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
internal struct MeanAgg
|
||||||
|
{
|
||||||
|
private double mean;
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
internal void Add(double n)
|
||||||
|
{
|
||||||
|
int c = count + 1;
|
||||||
|
mean = mean * count / c + n / c;
|
||||||
|
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly double Compute()
|
||||||
|
{
|
||||||
|
return mean;
|
||||||
|
}
|
||||||
|
}
|
65
src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanIQRAgg.cs
Normal file
65
src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanIQRAgg.cs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
internal struct MeanIQRAgg : IAggregator
|
||||||
|
{
|
||||||
|
private const int minLn = 4;
|
||||||
|
internal Collection<double> arr = [];
|
||||||
|
|
||||||
|
public MeanIQRAgg()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly void Add(double d)
|
||||||
|
{
|
||||||
|
arr.Add(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly double Compute()
|
||||||
|
{
|
||||||
|
var length = arr.Count;
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sorted = arr.OrderBy(p => p).ToArray();
|
||||||
|
|
||||||
|
double minV, maxV;
|
||||||
|
|
||||||
|
if (arr.Count < minLn)
|
||||||
|
{
|
||||||
|
minV = sorted[0];
|
||||||
|
maxV = sorted[length - 1];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var start = length / minLn;
|
||||||
|
var end = length * 3 / minLn - 1;
|
||||||
|
|
||||||
|
minV = sorted[start];
|
||||||
|
maxV = sorted[end];
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
double sum = 0;
|
||||||
|
|
||||||
|
foreach (var e in sorted)
|
||||||
|
{
|
||||||
|
if (e >= minV && e <= maxV)
|
||||||
|
{
|
||||||
|
sum += e;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum / count;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly void Clear()
|
||||||
|
{
|
||||||
|
arr.Clear();
|
||||||
|
}
|
||||||
|
}
|
27
src/FrostFS.SDK.Client/Models/Netmap/Placement/MinAgg.cs
Normal file
27
src/FrostFS.SDK.Client/Models/Netmap/Placement/MinAgg.cs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
internal struct MinAgg
|
||||||
|
{
|
||||||
|
private double min;
|
||||||
|
private bool minFound;
|
||||||
|
|
||||||
|
internal void Add(double n)
|
||||||
|
{
|
||||||
|
if (!minFound)
|
||||||
|
{
|
||||||
|
min = n;
|
||||||
|
minFound = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n < min)
|
||||||
|
{
|
||||||
|
min = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly double Compute()
|
||||||
|
{
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
}
|
16
src/FrostFS.SDK.Client/Models/Netmap/Placement/Operation.cs
Normal file
16
src/FrostFS.SDK.Client/Models/Netmap/Placement/Operation.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
public enum Operation
|
||||||
|
{
|
||||||
|
Unspecified = 0,
|
||||||
|
EQ,
|
||||||
|
NE,
|
||||||
|
GT,
|
||||||
|
GE,
|
||||||
|
LT,
|
||||||
|
LE,
|
||||||
|
OR,
|
||||||
|
AND,
|
||||||
|
NOT,
|
||||||
|
LIKE
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
internal struct ReverseMinNorm : INormalizer
|
||||||
|
{
|
||||||
|
internal double min;
|
||||||
|
|
||||||
|
public readonly double Normalize(double w) => (min + 1) / (w + 1);
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
internal struct SelectFilterExpr(uint cbf, FrostFsSelector selector, Collection<FrostFsFilter> filters)
|
||||||
|
{
|
||||||
|
internal uint Cbf { get; } = cbf;
|
||||||
|
internal FrostFsSelector Selector { get; } = selector;
|
||||||
|
internal Collection<FrostFsFilter> Filters { get; } = filters;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
internal readonly struct SigmoidNorm : INormalizer
|
||||||
|
{
|
||||||
|
private readonly double _scale;
|
||||||
|
|
||||||
|
internal SigmoidNorm(double scale)
|
||||||
|
{
|
||||||
|
_scale = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly double Normalize(double w)
|
||||||
|
{
|
||||||
|
if (_scale == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = w / _scale;
|
||||||
|
|
||||||
|
return x / (1 + x);
|
||||||
|
}
|
||||||
|
}
|
138
src/FrostFS.SDK.Client/Models/Netmap/Placement/Tools.cs
Normal file
138
src/FrostFS.SDK.Client/Models/Netmap/Placement/Tools.cs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
using static FrostFS.SDK.FrostFsNetmapSnapshot;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
|
||||||
|
|
||||||
|
public static class Tools
|
||||||
|
{
|
||||||
|
internal static ulong Distance(ulong x, ulong y)
|
||||||
|
{
|
||||||
|
var acc = x ^ y;
|
||||||
|
acc ^= acc >> 33;
|
||||||
|
acc *= 0xff51afd7ed558ccd;
|
||||||
|
acc ^= acc >> 33;
|
||||||
|
acc *= 0xc4ceb9fe1a85ec53;
|
||||||
|
acc ^= acc >> 33;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static double ReverceNormalize(double r, double w)
|
||||||
|
{
|
||||||
|
return (r + 1) / (w + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static double Normalize(double r, double w)
|
||||||
|
{
|
||||||
|
if (r == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = w / r;
|
||||||
|
return x / (1 + x);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void AppendWeightsTo(FrostFsNodeInfo[] nodes, Func<FrostFsNodeInfo, double> wf, ref double[] weights)
|
||||||
|
{
|
||||||
|
if (weights.Length < nodes.Length)
|
||||||
|
{
|
||||||
|
weights = new double[nodes.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < nodes.Length; i++)
|
||||||
|
{
|
||||||
|
weights[i] = wf(nodes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static List<T> SortHasherSliceByWeightValue<T>(List<T> nodes, Span<double> weights, ulong hash) where T : IHasher
|
||||||
|
{
|
||||||
|
if (nodes.Count == 0)
|
||||||
|
{
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allEquals = true;
|
||||||
|
|
||||||
|
if (weights.Length > 1)
|
||||||
|
{
|
||||||
|
for (int i = 1; i < weights.Length; i++)
|
||||||
|
{
|
||||||
|
if (weights[i] != weights[0])
|
||||||
|
{
|
||||||
|
allEquals = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dist = new double[nodes.Count];
|
||||||
|
|
||||||
|
if (allEquals)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < dist.Length; i++)
|
||||||
|
{
|
||||||
|
var x = nodes[i].Hash();
|
||||||
|
dist[i] = Distance(x, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SortHasherByDistance(nodes, dist, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < dist.Length; i++)
|
||||||
|
{
|
||||||
|
var d = Distance(nodes[i].Hash(), hash);
|
||||||
|
dist[i] = (ulong.MaxValue - d) * weights[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return SortHasherByDistance(nodes, dist, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static List<T> SortHasherByDistance<T, N>(List<T> nodes, N[] dist, bool asc)
|
||||||
|
{
|
||||||
|
IndexedValue<T, N>[] indexes = new IndexedValue<T, N>[nodes.Count];
|
||||||
|
for (int i = 0; i < dist.Length; i++)
|
||||||
|
{
|
||||||
|
indexes[i] = new IndexedValue<T, N>() { nodeInfo = nodes[i], dist = dist[i] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asc)
|
||||||
|
{
|
||||||
|
return new List<T>(indexes
|
||||||
|
.OrderBy(x => x.dist)
|
||||||
|
.Select(x => x.nodeInfo).ToArray());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new List<T>(indexes
|
||||||
|
.OrderByDescending(x => x.dist)
|
||||||
|
.Select(x => x.nodeInfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Func<FrostFsNodeInfo, double> DefaultWeightFunc(IReadOnlyList<FrostFsNodeInfo> nodes)
|
||||||
|
{
|
||||||
|
MeanAgg mean = new();
|
||||||
|
MinAgg minV = new();
|
||||||
|
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
mean.Add(node.GetCapacity());
|
||||||
|
minV.Add(node.Price);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewWeightFunc(
|
||||||
|
NewSigmoidNorm(mean.Compute()),
|
||||||
|
NewReverseMinNorm(minV.Compute()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct IndexedValue<T, N>
|
||||||
|
{
|
||||||
|
internal T nodeInfo;
|
||||||
|
internal N dist;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,36 @@
|
||||||
namespace FrostFS.SDK;
|
namespace FrostFS.SDK;
|
||||||
|
|
||||||
public class FrostFsAttributePair(string key, string value)
|
public struct FrostFsAttributePair(string key, string value) : System.IEquatable<FrostFsAttributePair>
|
||||||
{
|
{
|
||||||
public string Key { get; set; } = key;
|
public string Key { get; set; } = key;
|
||||||
|
|
||||||
public string Value { get; set; } = value;
|
public string Value { get; set; } = value;
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null || obj is not FrostFsAttributePair)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Equals((FrostFsAttributePair)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return Key.GetHashCode() ^ Value.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(FrostFsAttributePair left, FrostFsAttributePair right)
|
||||||
|
{
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(FrostFsAttributePair left, FrostFsAttributePair right)
|
||||||
|
{
|
||||||
|
return !(left == right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(FrostFsAttributePair other)
|
||||||
|
{
|
||||||
|
return GetHashCode().Equals(other.GetHashCode());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,6 @@ namespace FrostFS.SDK;
|
||||||
|
|
||||||
public class FrostFsObject
|
public class FrostFsObject
|
||||||
{
|
{
|
||||||
private byte[]? bytes;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates new instance from <c>ObjectHeader</c>
|
/// Creates new instance from <c>ObjectHeader</c>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -45,20 +43,12 @@ public class FrostFsObject
|
||||||
/// <value>Reader for received data</value>
|
/// <value>Reader for received data</value>
|
||||||
public IObjectReader? ObjectReader { get; set; }
|
public IObjectReader? ObjectReader { get; set; }
|
||||||
|
|
||||||
internal byte[] SingleObjectPayload
|
public ReadOnlyMemory<byte> SingleObjectPayload { get; set; }
|
||||||
{
|
|
||||||
get { return bytes ?? []; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The size of payload cannot exceed <c>MaxObjectSize</c> value from <c>NetworkSettings</c>
|
/// Provide SHA256 hash of the payload. If null, the hash is calculated by internal logic
|
||||||
/// Used only for PutSingleObject method
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>Buffer for output data</value>
|
public byte[]? PayloadHash { get; set; }
|
||||||
public void SetSingleObjectPayload(byte[] bytes)
|
|
||||||
{
|
|
||||||
this.bytes = bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applied only for the last Object in chain in case of manual multipart uploading
|
/// Applied only for the last Object in chain in case of manual multipart uploading
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using FrostFS.Object;
|
||||||
|
using FrostFS.SDK.Client.Mappers.GRPC;
|
||||||
|
|
||||||
namespace FrostFS.SDK;
|
namespace FrostFS.SDK;
|
||||||
|
|
||||||
|
@ -9,6 +12,8 @@ public class FrostFsSplit(SplitId splitId,
|
||||||
FrostFsSignature? parentSignature = null,
|
FrostFsSignature? parentSignature = null,
|
||||||
ReadOnlyCollection<FrostFsObjectId>? children = null)
|
ReadOnlyCollection<FrostFsObjectId>? children = null)
|
||||||
{
|
{
|
||||||
|
private Header.Types.Split? _split;
|
||||||
|
|
||||||
public FrostFsSplit() : this(new SplitId())
|
public FrostFsSplit() : this(new SplitId())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -24,4 +29,25 @@ public class FrostFsSplit(SplitId splitId,
|
||||||
public FrostFsObjectHeader? ParentHeader { get; set; } = parentHeader;
|
public FrostFsObjectHeader? ParentHeader { get; set; } = parentHeader;
|
||||||
|
|
||||||
public ReadOnlyCollection<FrostFsObjectId>? Children { get; } = children;
|
public ReadOnlyCollection<FrostFsObjectId>? Children { get; } = children;
|
||||||
|
|
||||||
|
public Header.Types.Split GetSplit()
|
||||||
|
{
|
||||||
|
if (_split == null)
|
||||||
|
{
|
||||||
|
_split = new Header.Types.Split
|
||||||
|
{
|
||||||
|
SplitId = SplitId?.GetSplitId(),
|
||||||
|
Parent = Parent?.ToMessage(),
|
||||||
|
ParentHeader = ParentHeader?.GetHeader(),
|
||||||
|
ParentSignature = ParentSignature?.ToMessage()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Children != null)
|
||||||
|
{
|
||||||
|
_split.Children.AddRange(Children.Select(x => x.ToMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _split;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,9 @@ public class FrostFsSplitInfo
|
||||||
|
|
||||||
public SplitId SplitId => _splitId ??= new SplitId(_splitInfo.SplitId.ToUuid());
|
public SplitId SplitId => _splitId ??= new SplitId(_splitInfo.SplitId.ToUuid());
|
||||||
|
|
||||||
public FrostFsObjectId Link => _link ??= FrostFsObjectId.FromHash(_splitInfo.Link.Value.Span);
|
public FrostFsObjectId? Link => _link ??= _splitInfo.Link == null
|
||||||
|
? null : FrostFsObjectId.FromHash(_splitInfo.Link.Value.Span);
|
||||||
|
|
||||||
public FrostFsObjectId LastPart => _lastPart ??= FrostFsObjectId.FromHash(_splitInfo.LastPart.Value.Span);
|
public FrostFsObjectId? LastPart => _lastPart ??= _splitInfo.LastPart == null
|
||||||
|
? null : FrostFsObjectId.FromHash(_splitInfo.LastPart.Value.Span);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public class PartUploadedEventArgs(ObjectPartInfo part) : EventArgs
|
||||||
|
{
|
||||||
|
public ObjectPartInfo Part { get; } = part;
|
||||||
|
}
|
|
@ -47,16 +47,11 @@ public class SplitId
|
||||||
return this.id.ToString();
|
return this.id.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[]? ToBinary()
|
|
||||||
{
|
|
||||||
if (this.id == Guid.Empty)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return this.id.ToBytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ByteString? GetSplitId()
|
public ByteString? GetSplitId()
|
||||||
{
|
{
|
||||||
return this.message ??= ByteString.CopyFrom(ToBinary());
|
Span<byte> span = stackalloc byte[16];
|
||||||
|
id.ToBytes(span);
|
||||||
|
|
||||||
|
return this.message ??= ByteString.CopyFrom(span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
38
src/FrostFS.SDK.Client/Models/Object/UploadInfo.cs
Normal file
38
src/FrostFS.SDK.Client/Models/Object/UploadInfo.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
using FrostFS.SDK;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public readonly struct ObjectPartInfo(long offset, int length, FrostFsObjectId objectId) : System.IEquatable<ObjectPartInfo>
|
||||||
|
{
|
||||||
|
public long Offset { get; } = offset;
|
||||||
|
public int Length { get; } = length;
|
||||||
|
public FrostFsObjectId ObjectId { get; } = objectId;
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null || obj is not ObjectPartInfo)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Equals((ObjectPartInfo)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return ((int)(Offset >> 32)) ^ (int)Offset ^ Length ^ ObjectId.Value.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(ObjectPartInfo left, ObjectPartInfo right)
|
||||||
|
{
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(ObjectPartInfo left, ObjectPartInfo right)
|
||||||
|
{
|
||||||
|
return !(left == right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(ObjectPartInfo other)
|
||||||
|
{
|
||||||
|
return GetHashCode() == other.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
48
src/FrostFS.SDK.Client/Models/Object/UploadProgressInfo.cs
Normal file
48
src/FrostFS.SDK.Client/Models/Object/UploadProgressInfo.cs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public class UploadProgressInfo
|
||||||
|
{
|
||||||
|
private List<ObjectPartInfo> _parts;
|
||||||
|
|
||||||
|
public UploadProgressInfo(Guid splitId, int capacity = 8)
|
||||||
|
{
|
||||||
|
_parts = new List<ObjectPartInfo>(capacity);
|
||||||
|
SplitId = new SplitId(splitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UploadProgressInfo(Guid splitId, Collection<ObjectPartInfo> parts)
|
||||||
|
{
|
||||||
|
_parts = [.. parts];
|
||||||
|
SplitId = new SplitId(splitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<PartUploadedEventArgs>? Notify;
|
||||||
|
|
||||||
|
public SplitId SplitId { get; }
|
||||||
|
|
||||||
|
internal void AddPart(ObjectPartInfo part)
|
||||||
|
{
|
||||||
|
_parts.Add(part);
|
||||||
|
Notify?.Invoke(this, new PartUploadedEventArgs(part));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectPartInfo GetPart(int index)
|
||||||
|
{
|
||||||
|
return _parts[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectPartInfo GetLast()
|
||||||
|
{
|
||||||
|
return _parts.LastOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyCollection<ObjectPartInfo> GetParts()
|
||||||
|
{
|
||||||
|
return new ReadOnlyCollection<ObjectPartInfo>(_parts);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,16 @@
|
||||||
namespace FrostFS.SDK;
|
namespace FrostFS.SDK;
|
||||||
|
|
||||||
public class FrostFsResponseStatus(FrostFsStatusCode code, string? message = null)
|
public class FrostFsResponseStatus(FrostFsStatusCode code, string? message = null, string? details = null)
|
||||||
{
|
{
|
||||||
public FrostFsStatusCode Code { get; set; } = code;
|
public FrostFsStatusCode Code { get; set; } = code;
|
||||||
public string Message { get; set; } = message ?? string.Empty;
|
public string Message { get; set; } = message ?? string.Empty;
|
||||||
|
|
||||||
|
public string Details { get; set; } = details ?? string.Empty;
|
||||||
|
|
||||||
public bool IsSuccess => Code == FrostFsStatusCode.Success;
|
public bool IsSuccess => Code == FrostFsStatusCode.Success;
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"Response status: {Code}. Message: {Message}.";
|
return $"Response status: {Code}. Message: {Message}. Details: {Details}";
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
using FrostFS.Refs;
|
using FrostFS.Refs;
|
||||||
|
|
||||||
using FrostFS.SDK.Client;
|
using FrostFS.SDK.Client;
|
||||||
|
@ -36,7 +35,9 @@ public class FrostFsSessionToken
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_id == Guid.Empty)
|
if (_id == Guid.Empty)
|
||||||
|
{
|
||||||
_id = ProtoId.ToUuid();
|
_id = ProtoId.ToUuid();
|
||||||
|
}
|
||||||
|
|
||||||
return _id;
|
return _id;
|
||||||
}
|
}
|
||||||
|
@ -47,7 +48,9 @@ public class FrostFsSessionToken
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_sessionKey.IsEmpty)
|
if (_sessionKey.IsEmpty)
|
||||||
|
{
|
||||||
_sessionKey = ProtoSessionKey.Memory;
|
_sessionKey = ProtoSessionKey.Memory;
|
||||||
|
}
|
||||||
|
|
||||||
return _sessionKey;
|
return _sessionKey;
|
||||||
}
|
}
|
||||||
|
@ -69,13 +72,16 @@ public class FrostFsSessionToken
|
||||||
sessionToken.Body.Container = new() { Verb = verb };
|
sessionToken.Body.Container = new() { Verb = verb };
|
||||||
|
|
||||||
if (containerId != null)
|
if (containerId != null)
|
||||||
|
{
|
||||||
sessionToken.Body.Container.ContainerId = containerId;
|
sessionToken.Body.Container.ContainerId = containerId;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
sessionToken.Body.Container.Wildcard = true;
|
sessionToken.Body.Container.Wildcard = true;
|
||||||
|
}
|
||||||
|
|
||||||
sessionToken.Body.SessionKey = key.PublicKeyProto;
|
sessionToken.Body.SessionKey = key.PublicKeyProto;
|
||||||
|
sessionToken.Signature = key.SignMessagePart(sessionToken.Body);
|
||||||
sessionToken.Signature = key.ECDsaKey.SignMessagePart(sessionToken.Body);
|
|
||||||
|
|
||||||
return sessionToken;
|
return sessionToken;
|
||||||
}
|
}
|
||||||
|
@ -100,7 +106,9 @@ public class FrostFsSessionToken
|
||||||
ObjectSessionContext.Types.Target target = new() { Container = address.ContainerId };
|
ObjectSessionContext.Types.Target target = new() { Container = address.ContainerId };
|
||||||
|
|
||||||
if (address.ObjectId != null)
|
if (address.ObjectId != null)
|
||||||
|
{
|
||||||
target.Objects.Add(address.ObjectId);
|
target.Objects.Add(address.ObjectId);
|
||||||
|
}
|
||||||
|
|
||||||
sessionToken.Body.Object = new()
|
sessionToken.Body.Object = new()
|
||||||
{
|
{
|
||||||
|
@ -108,9 +116,7 @@ public class FrostFsSessionToken
|
||||||
Verb = verb
|
Verb = verb
|
||||||
};
|
};
|
||||||
|
|
||||||
sessionToken.Body.SessionKey = key.PublicKeyProto;
|
sessionToken.Signature = key.SignMessagePart(sessionToken.Body);
|
||||||
|
|
||||||
sessionToken.Signature = key.ECDsaKey.SignMessagePart(sessionToken.Body);
|
|
||||||
|
|
||||||
return sessionToken;
|
return sessionToken;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,9 @@ namespace FrostFS.SDK.Client
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
chunkRequest.Sign(this.ctx.Key.ECDsaKey);
|
chunkRequest.AddMetaHeader(args.XHeaders);
|
||||||
|
|
||||||
|
chunkRequest.Sign(this.ctx.Key);
|
||||||
|
|
||||||
await streamer.Write(chunkRequest).ConfigureAwait(false);
|
await streamer.Write(chunkRequest).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
public readonly struct PrmApeChainRemove(
|
public readonly struct PrmApeChainRemove(
|
||||||
FrostFsChainTarget target,
|
FrostFsChainTarget target,
|
||||||
FrostFsChain chain,
|
byte[] chainId,
|
||||||
string[]? xheaders = null) : System.IEquatable<PrmApeChainRemove>
|
string[]? xheaders = null) : System.IEquatable<PrmApeChainRemove>
|
||||||
{
|
{
|
||||||
public FrostFsChainTarget Target { get; } = target;
|
public FrostFsChainTarget Target { get; } = target;
|
||||||
|
|
||||||
public FrostFsChain Chain { get; } = chain;
|
public byte[] ChainId { get; } = chainId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// FrostFS request X-Headers
|
/// FrostFS request X-Headers
|
||||||
|
@ -25,13 +25,13 @@ public readonly struct PrmApeChainRemove(
|
||||||
public readonly bool Equals(PrmApeChainRemove other)
|
public readonly bool Equals(PrmApeChainRemove other)
|
||||||
{
|
{
|
||||||
return Target == other.Target
|
return Target == other.Target
|
||||||
&& Chain == other.Chain
|
&& ChainId.Equals(other.ChainId)
|
||||||
&& XHeaders == other.XHeaders;
|
&& XHeaders == other.XHeaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override readonly int GetHashCode()
|
public override readonly int GetHashCode()
|
||||||
{
|
{
|
||||||
return Chain.GetHashCode() ^ Target.GetHashCode() ^ XHeaders.GetHashCode();
|
return ChainId.GetHashCode() ^ Target.GetHashCode() ^ XHeaders.GetHashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool operator ==(PrmApeChainRemove left, PrmApeChainRemove right)
|
public static bool operator ==(PrmApeChainRemove left, PrmApeChainRemove right)
|
||||||
|
|
|
@ -8,7 +8,8 @@ public readonly struct PrmObjectClientCutPut(
|
||||||
int bufferMaxSize = 0,
|
int bufferMaxSize = 0,
|
||||||
FrostFsSessionToken? sessionToken = null,
|
FrostFsSessionToken? sessionToken = null,
|
||||||
byte[]? customBuffer = null,
|
byte[]? customBuffer = null,
|
||||||
string[]? xheaders = null) : PrmObjectPutBase, System.IEquatable<PrmObjectClientCutPut>
|
string[]? xheaders = null,
|
||||||
|
UploadProgressInfo? progress = null) : PrmObjectPutBase, System.IEquatable<PrmObjectClientCutPut>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Need to provide values like <c>ContainerId</c> and <c>ObjectType</c> to create and object.
|
/// Need to provide values like <c>ContainerId</c> and <c>ObjectType</c> to create and object.
|
||||||
|
@ -41,6 +42,8 @@ public readonly struct PrmObjectClientCutPut(
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] XHeaders { get; } = xheaders ?? [];
|
public string[] XHeaders { get; } = xheaders ?? [];
|
||||||
|
|
||||||
|
public UploadProgressInfo? Progress { get; } = progress;
|
||||||
|
|
||||||
internal PutObjectContext PutObjectContext { get; } = new();
|
internal PutObjectContext PutObjectContext { get; } = new();
|
||||||
|
|
||||||
public override readonly bool Equals(object obj)
|
public override readonly bool Equals(object obj)
|
||||||
|
|
|
@ -1,163 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
// clientStatusMonitor count error rate and other statistics for connection.
|
|
||||||
public class ClientStatusMonitor : IClientStatus
|
|
||||||
{
|
|
||||||
private static readonly MethodIndex[] MethodIndexes =
|
|
||||||
[
|
|
||||||
MethodIndex.methodBalanceGet,
|
|
||||||
MethodIndex.methodContainerPut,
|
|
||||||
MethodIndex.methodContainerGet,
|
|
||||||
MethodIndex.methodContainerList,
|
|
||||||
MethodIndex.methodContainerDelete,
|
|
||||||
MethodIndex.methodEndpointInfo,
|
|
||||||
MethodIndex.methodNetworkInfo,
|
|
||||||
MethodIndex.methodNetMapSnapshot,
|
|
||||||
MethodIndex.methodObjectPut,
|
|
||||||
MethodIndex.methodObjectDelete,
|
|
||||||
MethodIndex.methodObjectGet,
|
|
||||||
MethodIndex.methodObjectHead,
|
|
||||||
MethodIndex.methodObjectRange,
|
|
||||||
MethodIndex.methodObjectPatch,
|
|
||||||
MethodIndex.methodSessionCreate,
|
|
||||||
MethodIndex.methodAPEManagerAddChain,
|
|
||||||
MethodIndex.methodAPEManagerRemoveChain,
|
|
||||||
MethodIndex.methodAPEManagerListChains
|
|
||||||
];
|
|
||||||
|
|
||||||
public static string GetMethodName(MethodIndex index)
|
|
||||||
{
|
|
||||||
return index switch
|
|
||||||
{
|
|
||||||
MethodIndex.methodBalanceGet => "BalanceGet",
|
|
||||||
MethodIndex.methodContainerPut => "ContainerPut",
|
|
||||||
MethodIndex.methodContainerGet => "ContainerGet",
|
|
||||||
MethodIndex.methodContainerList => "ContainerList",
|
|
||||||
MethodIndex.methodContainerDelete => "ContainerDelete",
|
|
||||||
MethodIndex.methodEndpointInfo => "EndpointInfo",
|
|
||||||
MethodIndex.methodNetworkInfo => "NetworkInfo",
|
|
||||||
MethodIndex.methodNetMapSnapshot => "NetMapSnapshot",
|
|
||||||
MethodIndex.methodObjectPut => "ObjectPut",
|
|
||||||
MethodIndex.methodObjectDelete => "ObjectDelete",
|
|
||||||
MethodIndex.methodObjectGet => "ObjectGet",
|
|
||||||
MethodIndex.methodObjectHead => "ObjectHead",
|
|
||||||
MethodIndex.methodObjectRange => "ObjectRange",
|
|
||||||
MethodIndex.methodObjectPatch => "ObjectPatch",
|
|
||||||
MethodIndex.methodSessionCreate => "SessionCreate",
|
|
||||||
MethodIndex.methodAPEManagerAddChain => "APEManagerAddChain",
|
|
||||||
MethodIndex.methodAPEManagerRemoveChain => "APEManagerRemoveChain",
|
|
||||||
MethodIndex.methodAPEManagerListChains => "APEManagerListChains",
|
|
||||||
_ => throw new ArgumentException("Unknown method", nameof(index)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
private readonly ILogger? logger;
|
|
||||||
private int healthy;
|
|
||||||
|
|
||||||
public ClientStatusMonitor(ILogger? logger, string address)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
healthy = (int)HealthyStatus.Healthy;
|
|
||||||
|
|
||||||
Address = address;
|
|
||||||
Methods = new MethodStatus[MethodIndexes.Length];
|
|
||||||
|
|
||||||
for (int i = 0; i < MethodIndexes.Length; i++)
|
|
||||||
{
|
|
||||||
Methods[i] = new MethodStatus(GetMethodName(MethodIndexes[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Address { get; }
|
|
||||||
|
|
||||||
internal uint ErrorThreshold { get; set; }
|
|
||||||
|
|
||||||
public uint CurrentErrorCount { get; set; }
|
|
||||||
|
|
||||||
public ulong OverallErrorCount { get; set; }
|
|
||||||
|
|
||||||
public MethodStatus[] Methods { get; private set; }
|
|
||||||
|
|
||||||
public bool IsHealthy()
|
|
||||||
{
|
|
||||||
var res = Interlocked.CompareExchange(ref healthy, -1, -1) == (int)HealthyStatus.Healthy;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsDialed()
|
|
||||||
{
|
|
||||||
return Interlocked.CompareExchange(ref healthy, -1, -1) != (int)HealthyStatus.UnhealthyOnDial;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetHealthy()
|
|
||||||
{
|
|
||||||
Interlocked.Exchange(ref healthy, (int)HealthyStatus.Healthy);
|
|
||||||
}
|
|
||||||
public void SetUnhealthy()
|
|
||||||
{
|
|
||||||
Interlocked.Exchange(ref healthy, (int)HealthyStatus.UnhealthyOnRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetUnhealthyOnDial()
|
|
||||||
{
|
|
||||||
Interlocked.Exchange(ref healthy, (int)HealthyStatus.UnhealthyOnDial);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void IncErrorRate()
|
|
||||||
{
|
|
||||||
bool thresholdReached;
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
CurrentErrorCount++;
|
|
||||||
OverallErrorCount++;
|
|
||||||
|
|
||||||
thresholdReached = CurrentErrorCount >= ErrorThreshold;
|
|
||||||
|
|
||||||
if (thresholdReached)
|
|
||||||
{
|
|
||||||
SetUnhealthy();
|
|
||||||
CurrentErrorCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thresholdReached && logger != null)
|
|
||||||
{
|
|
||||||
FrostFsMessages.ErrorЕhresholdReached(logger, Address, ErrorThreshold);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public uint GetCurrentErrorRate()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return CurrentErrorCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ulong GetOverallErrorRate()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return OverallErrorCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public StatusSnapshot[] MethodsStatus()
|
|
||||||
{
|
|
||||||
var result = new StatusSnapshot[Methods.Length];
|
|
||||||
|
|
||||||
for (int i = 0; i < result.Length; i++)
|
|
||||||
{
|
|
||||||
result[i] = Methods[i].Snapshot!;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Grpc.Core;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
// clientWrapper is used by default, alternative implementations are intended for testing purposes only.
|
|
||||||
public class ClientWrapper : ClientStatusMonitor
|
|
||||||
{
|
|
||||||
private readonly object _lock = new();
|
|
||||||
private SessionCache sessionCache;
|
|
||||||
|
|
||||||
internal ClientWrapper(WrapperPrm wrapperPrm, Pool pool) : base(wrapperPrm.Logger, wrapperPrm.Address)
|
|
||||||
{
|
|
||||||
WrapperPrm = wrapperPrm;
|
|
||||||
ErrorThreshold = wrapperPrm.ErrorThreshold;
|
|
||||||
|
|
||||||
sessionCache = pool.SessionCache;
|
|
||||||
Client = new FrostFSClient(WrapperPrm, sessionCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal FrostFSClient? Client { get; private set; }
|
|
||||||
|
|
||||||
internal WrapperPrm WrapperPrm { get; }
|
|
||||||
|
|
||||||
internal FrostFSClient? GetClient()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (IsHealthy())
|
|
||||||
{
|
|
||||||
return Client;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dial establishes a connection to the server from the FrostFS network.
|
|
||||||
// Returns an error describing failure reason. If failed, the client
|
|
||||||
// SHOULD NOT be used.
|
|
||||||
internal async Task Dial(CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = GetClient() ?? throw new FrostFsInvalidObjectException("pool client unhealthy");
|
|
||||||
|
|
||||||
await client.Dial(ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void HandleError(Exception ex)
|
|
||||||
{
|
|
||||||
if (ex is FrostFsResponseException responseException && responseException.Status != null)
|
|
||||||
{
|
|
||||||
switch (responseException.Status.Code)
|
|
||||||
{
|
|
||||||
case FrostFsStatusCode.Internal:
|
|
||||||
case FrostFsStatusCode.WrongMagicNumber:
|
|
||||||
case FrostFsStatusCode.SignatureVerificationFailure:
|
|
||||||
case FrostFsStatusCode.NodeUnderMaintenance:
|
|
||||||
IncErrorRate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IncErrorRate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ScheduleGracefulClose()
|
|
||||||
{
|
|
||||||
if (Client == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await Task.Delay((int)WrapperPrm.GracefulCloseOnSwitchTimeout).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// restartIfUnhealthy checks healthy status of client and recreate it if status is unhealthy.
|
|
||||||
// Indicating if status was changed by this function call and returns error that caused unhealthy status.
|
|
||||||
internal async Task<bool> RestartIfUnhealthy(CallContext ctx)
|
|
||||||
{
|
|
||||||
bool wasHealthy;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await Client!.GetNodeInfoAsync(ctx).ConfigureAwait(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (RpcException)
|
|
||||||
{
|
|
||||||
wasHealthy = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if connection is dialed before, to avoid routine/connection leak,
|
|
||||||
// pool has to close it and then initialize once again.
|
|
||||||
if (IsDialed())
|
|
||||||
{
|
|
||||||
await ScheduleGracefulClose().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
FrostFSClient? client = new(WrapperPrm, sessionCache);
|
|
||||||
|
|
||||||
var error = await client.Dial(ctx).ConfigureAwait(false);
|
|
||||||
if (!string.IsNullOrEmpty(error))
|
|
||||||
{
|
|
||||||
SetUnhealthyOnDial();
|
|
||||||
return wasHealthy;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
Client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var res = await Client.GetNodeInfoAsync(ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (FrostFsException)
|
|
||||||
{
|
|
||||||
SetUnhealthy();
|
|
||||||
|
|
||||||
return wasHealthy;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetHealthy();
|
|
||||||
return !wasHealthy;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void IncRequests(ulong elapsed, MethodIndex method)
|
|
||||||
{
|
|
||||||
var methodStat = Methods[(int)method];
|
|
||||||
|
|
||||||
methodStat.IncRequests(elapsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
// values for healthy status of clientStatusMonitor.
|
|
||||||
public enum HealthyStatus
|
|
||||||
{
|
|
||||||
// statusUnhealthyOnDial is set when dialing to the endpoint is failed,
|
|
||||||
// so there is no connection to the endpoint, and pool should not close it
|
|
||||||
// before re-establishing connection once again.
|
|
||||||
UnhealthyOnDial,
|
|
||||||
|
|
||||||
// statusUnhealthyOnRequest is set when communication after dialing to the
|
|
||||||
// endpoint is failed due to immediate or accumulated errors, connection is
|
|
||||||
// available and pool should close it before re-establishing connection once again.
|
|
||||||
UnhealthyOnRequest,
|
|
||||||
|
|
||||||
// statusHealthy is set when connection is ready to be used by the pool.
|
|
||||||
Healthy
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public interface IClientStatus
|
|
||||||
{
|
|
||||||
// isHealthy checks if the connection can handle requests.
|
|
||||||
bool IsHealthy();
|
|
||||||
|
|
||||||
// isDialed checks if the connection was created.
|
|
||||||
bool IsDialed();
|
|
||||||
|
|
||||||
// setUnhealthy marks client as unhealthy.
|
|
||||||
void SetUnhealthy();
|
|
||||||
|
|
||||||
// address return address of endpoint.
|
|
||||||
string Address { get; }
|
|
||||||
|
|
||||||
// currentErrorRate returns current errors rate.
|
|
||||||
// After specific threshold connection is considered as unhealthy.
|
|
||||||
// Pool.startRebalance routine can make this connection healthy again.
|
|
||||||
uint GetCurrentErrorRate();
|
|
||||||
|
|
||||||
// overallErrorRate returns the number of all happened errors.
|
|
||||||
ulong GetOverallErrorRate();
|
|
||||||
|
|
||||||
// methodsStatus returns statistic for all used methods.
|
|
||||||
StatusSnapshot[] MethodsStatus();
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
|
|
||||||
using Grpc.Core;
|
|
||||||
using Grpc.Core.Interceptors;
|
|
||||||
using Grpc.Net.Client;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
// InitParameters contains values used to initialize connection Pool.
|
|
||||||
public class InitParameters(Func<string, ChannelBase> grpcChannelFactory)
|
|
||||||
{
|
|
||||||
public ECDsa? Key { get; set; }
|
|
||||||
|
|
||||||
public ulong NodeDialTimeout { get; set; }
|
|
||||||
|
|
||||||
public ulong NodeStreamTimeout { get; set; }
|
|
||||||
|
|
||||||
public ulong HealthcheckTimeout { get; set; }
|
|
||||||
|
|
||||||
public ulong ClientRebalanceInterval { get; set; }
|
|
||||||
|
|
||||||
public ulong SessionExpirationDuration { get; set; }
|
|
||||||
|
|
||||||
public uint ErrorThreshold { get; set; }
|
|
||||||
|
|
||||||
public NodeParam[]? NodeParams { get; set; }
|
|
||||||
|
|
||||||
public GrpcChannelOptions[]? DialOptions { get; set; }
|
|
||||||
|
|
||||||
public Func<string, ClientWrapper>? ClientBuilder { get; set; }
|
|
||||||
|
|
||||||
public ulong GracefulCloseOnSwitchTimeout { get; set; }
|
|
||||||
|
|
||||||
public ILogger? Logger { get; set; }
|
|
||||||
|
|
||||||
public Action<CallStatistics>? Callback { get; set; }
|
|
||||||
|
|
||||||
public Collection<Interceptor> Interceptors { get; } = [];
|
|
||||||
|
|
||||||
public Func<string, ChannelBase> GrpcChannelFactory { get; set; } = grpcChannelFactory;
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
using FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
internal sealed class InnerPool
|
|
||||||
{
|
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
internal InnerPool(Sampler sampler, ClientWrapper[] clients)
|
|
||||||
{
|
|
||||||
Clients = clients;
|
|
||||||
Sampler = sampler;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal Sampler Sampler { get; set; }
|
|
||||||
|
|
||||||
internal ClientWrapper[] Clients { get; }
|
|
||||||
|
|
||||||
internal ClientWrapper? Connection()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (Clients.Length == 1)
|
|
||||||
{
|
|
||||||
var client = Clients[0];
|
|
||||||
if (client.IsHealthy())
|
|
||||||
{
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var attempts = 3 * Clients.Length;
|
|
||||||
|
|
||||||
for (int i = 0; i < attempts; i++)
|
|
||||||
{
|
|
||||||
int index = Sampler.Next();
|
|
||||||
|
|
||||||
if (Clients[index].IsHealthy())
|
|
||||||
{
|
|
||||||
return Clients[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public enum MethodIndex
|
|
||||||
{
|
|
||||||
methodBalanceGet,
|
|
||||||
methodContainerPut,
|
|
||||||
methodContainerGet,
|
|
||||||
methodContainerList,
|
|
||||||
methodContainerDelete,
|
|
||||||
methodEndpointInfo,
|
|
||||||
methodNetworkInfo,
|
|
||||||
methodNetMapSnapshot,
|
|
||||||
methodObjectPut,
|
|
||||||
methodObjectDelete,
|
|
||||||
methodObjectGet,
|
|
||||||
methodObjectHead,
|
|
||||||
methodObjectRange,
|
|
||||||
methodObjectPatch,
|
|
||||||
methodSessionCreate,
|
|
||||||
methodAPEManagerAddChain,
|
|
||||||
methodAPEManagerRemoveChain,
|
|
||||||
methodAPEManagerListChains,
|
|
||||||
methodLast
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public class MethodStatus(string name)
|
|
||||||
{
|
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
public string Name { get; } = name;
|
|
||||||
|
|
||||||
public StatusSnapshot Snapshot { get; set; } = new StatusSnapshot();
|
|
||||||
|
|
||||||
internal void IncRequests(ulong elapsed)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
Snapshot.AllTime += elapsed;
|
|
||||||
Snapshot.AllRequests++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
// NodeParam groups parameters of remote node.
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "<Pending>")]
|
|
||||||
public readonly struct NodeParam(int priority, string address, float weight)
|
|
||||||
{
|
|
||||||
public int Priority { get; } = priority;
|
|
||||||
|
|
||||||
public string Address { get; } = address;
|
|
||||||
|
|
||||||
public float Weight { get; } = weight;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public class NodeStatistic
|
|
||||||
{
|
|
||||||
public string? Address { get; internal set; }
|
|
||||||
|
|
||||||
public StatusSnapshot[]? Methods { get; internal set; }
|
|
||||||
|
|
||||||
public ulong OverallErrors { get; internal set; }
|
|
||||||
|
|
||||||
public uint CurrentErrors { get; internal set; }
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public class NodesParam(int priority)
|
|
||||||
{
|
|
||||||
public int Priority { get; } = priority;
|
|
||||||
|
|
||||||
public Collection<string> Addresses { get; } = [];
|
|
||||||
|
|
||||||
public Collection<double> Weights { get; } = [];
|
|
||||||
}
|
|
|
@ -1,668 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Frostfs.V2.Ape;
|
|
||||||
|
|
||||||
using FrostFS.Refs;
|
|
||||||
using FrostFS.SDK.Client.Interfaces;
|
|
||||||
using FrostFS.SDK.Client.Mappers.GRPC;
|
|
||||||
using FrostFS.SDK.Cryptography;
|
|
||||||
|
|
||||||
using Grpc.Core;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public partial class Pool : IFrostFSClient
|
|
||||||
{
|
|
||||||
const int defaultSessionTokenExpirationDuration = 100; // in epochs
|
|
||||||
|
|
||||||
const int defaultErrorThreshold = 100;
|
|
||||||
|
|
||||||
const int defaultGracefulCloseOnSwitchTimeout = 10; //Seconds;
|
|
||||||
const int defaultRebalanceInterval = 15; //Seconds;
|
|
||||||
const int defaultHealthcheckTimeout = 4; //Seconds;
|
|
||||||
const int defaultDialTimeout = 5; //Seconds;
|
|
||||||
const int defaultStreamTimeout = 10; //Seconds;
|
|
||||||
|
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
private InnerPool[]? InnerPools { get; set; }
|
|
||||||
|
|
||||||
private ClientKey Key { get; set; }
|
|
||||||
|
|
||||||
private OwnerID? _ownerId;
|
|
||||||
|
|
||||||
private FrostFsOwner? _owner;
|
|
||||||
|
|
||||||
private FrostFsOwner Owner
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
_owner ??= new FrostFsOwner(Key.ECDsaKey.PublicKey().PublicKeyToAddress());
|
|
||||||
return _owner;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private OwnerID OwnerId
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_ownerId == null)
|
|
||||||
{
|
|
||||||
_owner = Key.Owner;
|
|
||||||
_ownerId = _owner.ToMessage();
|
|
||||||
}
|
|
||||||
return _ownerId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource();
|
|
||||||
|
|
||||||
internal SessionCache SessionCache { get; set; }
|
|
||||||
|
|
||||||
private ulong SessionTokenDuration { get; set; }
|
|
||||||
|
|
||||||
private RebalanceParameters RebalanceParams { get; set; }
|
|
||||||
|
|
||||||
private Func<string, ClientWrapper> ClientBuilder;
|
|
||||||
|
|
||||||
private bool disposedValue;
|
|
||||||
|
|
||||||
private ILogger? logger { get; set; }
|
|
||||||
|
|
||||||
private ulong MaxObjectSize { get; set; }
|
|
||||||
|
|
||||||
public IClientStatus? ClientStatus { get; }
|
|
||||||
|
|
||||||
// NewPool creates connection pool using parameters.
|
|
||||||
public Pool(InitParameters options)
|
|
||||||
{
|
|
||||||
if (options is null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(options));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.Key == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Missed required parameter {nameof(options.Key)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodesParams = AdjustNodeParams(options.NodeParams);
|
|
||||||
|
|
||||||
var cache = new SessionCache(options.SessionExpirationDuration);
|
|
||||||
|
|
||||||
FillDefaultInitParams(options, this);
|
|
||||||
|
|
||||||
Key = new ClientKey(options.Key);
|
|
||||||
|
|
||||||
SessionCache = cache;
|
|
||||||
logger = options.Logger;
|
|
||||||
SessionTokenDuration = options.SessionExpirationDuration;
|
|
||||||
|
|
||||||
RebalanceParams = new RebalanceParameters(
|
|
||||||
nodesParams.ToArray(),
|
|
||||||
options.HealthcheckTimeout,
|
|
||||||
options.ClientRebalanceInterval,
|
|
||||||
options.SessionExpirationDuration);
|
|
||||||
|
|
||||||
ClientBuilder = options.ClientBuilder!;
|
|
||||||
|
|
||||||
// ClientContext.PoolErrorHandler = client.HandleError;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial establishes a connection to the servers from the FrostFS network.
|
|
||||||
// It also starts a routine that checks the health of the nodes and
|
|
||||||
// updates the weights of the nodes for balancing.
|
|
||||||
// Returns an error describing failure reason.
|
|
||||||
//
|
|
||||||
// If failed, the Pool SHOULD NOT be used.
|
|
||||||
//
|
|
||||||
// See also InitParameters.SetClientRebalanceInterval.
|
|
||||||
public async Task<string?> Dial(CallContext ctx)
|
|
||||||
{
|
|
||||||
var inner = new InnerPool[RebalanceParams.NodesParams.Length];
|
|
||||||
|
|
||||||
bool atLeastOneHealthy = false;
|
|
||||||
int i = 0;
|
|
||||||
foreach (var nodeParams in RebalanceParams.NodesParams)
|
|
||||||
{
|
|
||||||
var wrappers = new ClientWrapper[nodeParams.Weights.Count];
|
|
||||||
|
|
||||||
for (int j = 0; j < nodeParams.Addresses.Count; j++)
|
|
||||||
{
|
|
||||||
ClientWrapper? wrapper = null;
|
|
||||||
bool dialed = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
wrapper = wrappers[j] = ClientBuilder(nodeParams.Addresses[j]);
|
|
||||||
|
|
||||||
await wrapper.Dial(ctx).ConfigureAwait(false);
|
|
||||||
dialed = true;
|
|
||||||
|
|
||||||
var token = await InitSessionForDuration(ctx, wrapper, RebalanceParams.SessionExpirationDuration, Key.ECDsaKey, false)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var key = FormCacheKey(nodeParams.Addresses[j], Key.PublicKey);
|
|
||||||
SessionCache.SetValue(key, token);
|
|
||||||
|
|
||||||
atLeastOneHealthy = true;
|
|
||||||
}
|
|
||||||
catch (RpcException ex)
|
|
||||||
{
|
|
||||||
if (!dialed)
|
|
||||||
wrapper!.SetUnhealthyOnDial();
|
|
||||||
else
|
|
||||||
wrapper!.SetUnhealthy();
|
|
||||||
|
|
||||||
if (logger != null)
|
|
||||||
{
|
|
||||||
FrostFsMessages.SessionCreationError(logger, wrapper!.WrapperPrm.Address, ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (FrostFsInvalidObjectException)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sampler = new Sampler(nodeParams.Weights.ToArray());
|
|
||||||
|
|
||||||
inner[i] = new InnerPool(sampler, wrappers);
|
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!atLeastOneHealthy)
|
|
||||||
return "At least one node must be healthy";
|
|
||||||
|
|
||||||
InnerPools = inner;
|
|
||||||
|
|
||||||
var res = await GetNetworkSettingsAsync(default).ConfigureAwait(false);
|
|
||||||
|
|
||||||
MaxObjectSize = res.MaxObjectSize;
|
|
||||||
|
|
||||||
StartRebalance(ctx);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<NodesParam> AdjustNodeParams(NodeParam[]? nodeParams)
|
|
||||||
{
|
|
||||||
if (nodeParams == null || nodeParams.Length == 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("No FrostFS peers configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary<int, NodesParam> nodesParamsDict = new(nodeParams.Length);
|
|
||||||
foreach (var nodeParam in nodeParams)
|
|
||||||
{
|
|
||||||
if (!nodesParamsDict.TryGetValue(nodeParam.Priority, out var nodes))
|
|
||||||
{
|
|
||||||
nodes = new NodesParam(nodeParam.Priority);
|
|
||||||
nodesParamsDict[nodeParam.Priority] = nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes.Addresses.Add(nodeParam.Address);
|
|
||||||
nodes.Weights.Add(nodeParam.Weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodesParams = new List<NodesParam>(nodesParamsDict.Count);
|
|
||||||
|
|
||||||
foreach (var key in nodesParamsDict.Keys)
|
|
||||||
{
|
|
||||||
var nodes = nodesParamsDict[key];
|
|
||||||
var newWeights = AdjustWeights([.. nodes.Weights]);
|
|
||||||
nodes.Weights.Clear();
|
|
||||||
foreach (var weight in newWeights)
|
|
||||||
{
|
|
||||||
nodes.Weights.Add(weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
nodesParams.Add(nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodesParams.OrderBy(n => n.Priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double[] AdjustWeights(double[] weights)
|
|
||||||
{
|
|
||||||
var adjusted = new double[weights.Length];
|
|
||||||
|
|
||||||
var sum = weights.Sum();
|
|
||||||
|
|
||||||
if (sum > 0)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < weights.Length; i++)
|
|
||||||
{
|
|
||||||
adjusted[i] = weights[i] / sum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return adjusted;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void FillDefaultInitParams(InitParameters parameters, Pool pool)
|
|
||||||
{
|
|
||||||
if (parameters.SessionExpirationDuration == 0)
|
|
||||||
parameters.SessionExpirationDuration = defaultSessionTokenExpirationDuration;
|
|
||||||
|
|
||||||
if (parameters.ErrorThreshold == 0)
|
|
||||||
parameters.ErrorThreshold = defaultErrorThreshold;
|
|
||||||
|
|
||||||
if (parameters.ClientRebalanceInterval <= 0)
|
|
||||||
parameters.ClientRebalanceInterval = defaultRebalanceInterval;
|
|
||||||
|
|
||||||
if (parameters.GracefulCloseOnSwitchTimeout <= 0)
|
|
||||||
parameters.GracefulCloseOnSwitchTimeout = defaultGracefulCloseOnSwitchTimeout;
|
|
||||||
|
|
||||||
if (parameters.HealthcheckTimeout <= 0)
|
|
||||||
parameters.HealthcheckTimeout = defaultHealthcheckTimeout;
|
|
||||||
|
|
||||||
if (parameters.NodeDialTimeout <= 0)
|
|
||||||
parameters.NodeDialTimeout = defaultDialTimeout;
|
|
||||||
|
|
||||||
if (parameters.NodeStreamTimeout <= 0)
|
|
||||||
parameters.NodeStreamTimeout = defaultStreamTimeout;
|
|
||||||
|
|
||||||
if (parameters.SessionExpirationDuration == 0)
|
|
||||||
parameters.SessionExpirationDuration = defaultSessionTokenExpirationDuration;
|
|
||||||
|
|
||||||
parameters.ClientBuilder ??= new Func<string, ClientWrapper>((address) =>
|
|
||||||
{
|
|
||||||
var wrapperPrm = new WrapperPrm()
|
|
||||||
{
|
|
||||||
Address = address,
|
|
||||||
Key = parameters.Key!,
|
|
||||||
Logger = parameters.Logger,
|
|
||||||
DialTimeout = parameters.NodeDialTimeout,
|
|
||||||
StreamTimeout = parameters.NodeStreamTimeout,
|
|
||||||
ErrorThreshold = parameters.ErrorThreshold,
|
|
||||||
GracefulCloseOnSwitchTimeout = parameters.GracefulCloseOnSwitchTimeout,
|
|
||||||
Callback = parameters.Callback,
|
|
||||||
Interceptors = parameters.Interceptors,
|
|
||||||
GrpcChannelFactory = parameters.GrpcChannelFactory
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
return new ClientWrapper(wrapperPrm, pool);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClientWrapper Connection()
|
|
||||||
{
|
|
||||||
foreach (var pool in InnerPools!)
|
|
||||||
{
|
|
||||||
var client = pool.Connection();
|
|
||||||
if (client != null)
|
|
||||||
{
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new FrostFsException("Cannot find alive client");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<FrostFsSessionToken> InitSessionForDuration(CallContext ctx, ClientWrapper cw, ulong duration, ECDsa key, bool clientCut)
|
|
||||||
{
|
|
||||||
var client = cw.Client;
|
|
||||||
var networkInfo = await client!.GetNetworkSettingsAsync(ctx).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var epoch = networkInfo.Epoch;
|
|
||||||
|
|
||||||
ulong exp = ulong.MaxValue - epoch < duration
|
|
||||||
? ulong.MaxValue
|
|
||||||
: epoch + duration;
|
|
||||||
|
|
||||||
var prmSessionCreate = new PrmSessionCreate(exp);
|
|
||||||
|
|
||||||
return await client.CreateSessionAsync(prmSessionCreate, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string FormCacheKey(string address, string key)
|
|
||||||
{
|
|
||||||
return $"{address}{key}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Close()
|
|
||||||
{
|
|
||||||
CancellationTokenSource.Cancel();
|
|
||||||
|
|
||||||
//if (InnerPools != null)
|
|
||||||
//{
|
|
||||||
// // close all clients
|
|
||||||
// foreach (var innerPool in InnerPools)
|
|
||||||
// foreach (var client in innerPool.Clients)
|
|
||||||
// if (client.IsDialed())
|
|
||||||
// client.Client?.Close();
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
// startRebalance runs loop to monitor connection healthy status.
|
|
||||||
internal void StartRebalance(CallContext ctx)
|
|
||||||
{
|
|
||||||
var buffers = new double[RebalanceParams.NodesParams.Length][];
|
|
||||||
|
|
||||||
for (int i = 0; i < RebalanceParams.NodesParams.Length; i++)
|
|
||||||
{
|
|
||||||
var parameters = RebalanceParams.NodesParams[i];
|
|
||||||
buffers[i] = new double[parameters.Weights.Count];
|
|
||||||
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await Task.Delay((int)RebalanceParams.ClientRebalanceInterval).ConfigureAwait(false);
|
|
||||||
UpdateNodesHealth(ctx, buffers);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateNodesHealth(CallContext ctx, double[][] buffers)
|
|
||||||
{
|
|
||||||
var tasks = new Task[InnerPools!.Length];
|
|
||||||
|
|
||||||
for (int i = 0; i < InnerPools.Length; i++)
|
|
||||||
{
|
|
||||||
var bufferWeights = buffers[i];
|
|
||||||
|
|
||||||
tasks[i] = Task.Run(() => UpdateInnerNodesHealth(ctx, i, bufferWeights));
|
|
||||||
}
|
|
||||||
|
|
||||||
Task.WaitAll(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask UpdateInnerNodesHealth(CallContext ctx, int poolIndex, double[] bufferWeights)
|
|
||||||
{
|
|
||||||
if (poolIndex > InnerPools!.Length - 1)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pool = InnerPools[poolIndex];
|
|
||||||
|
|
||||||
var options = RebalanceParams;
|
|
||||||
|
|
||||||
int healthyChanged = 0;
|
|
||||||
|
|
||||||
var tasks = new Task[pool.Clients.Length];
|
|
||||||
|
|
||||||
for (int j = 0; j < pool.Clients.Length; j++)
|
|
||||||
{
|
|
||||||
var client = pool.Clients[j];
|
|
||||||
var healthy = false;
|
|
||||||
string? error = null;
|
|
||||||
var changed = false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// check timeout settings
|
|
||||||
changed = await client!.RestartIfUnhealthy(ctx).ConfigureAwait(false);
|
|
||||||
healthy = true;
|
|
||||||
bufferWeights[j] = options.NodesParams[poolIndex].Weights[j];
|
|
||||||
}
|
|
||||||
// TODO: specify
|
|
||||||
catch (FrostFsException e)
|
|
||||||
{
|
|
||||||
error = e.Message;
|
|
||||||
bufferWeights[j] = 0;
|
|
||||||
|
|
||||||
SessionCache.DeleteByPrefix(client.Address);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(error))
|
|
||||||
{
|
|
||||||
if (logger != null)
|
|
||||||
{
|
|
||||||
FrostFsMessages.HealthChanged(logger, client.Address, healthy, error!);
|
|
||||||
}
|
|
||||||
|
|
||||||
Interlocked.Exchange(ref healthyChanged, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (Interlocked.CompareExchange(ref healthyChanged, -1, -1) == 1)
|
|
||||||
{
|
|
||||||
var probabilities = AdjustWeights(bufferWeights);
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
pool.Sampler = new Sampler(probabilities);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: remove
|
|
||||||
private bool CheckSessionTokenErr(Exception error, string address)
|
|
||||||
{
|
|
||||||
if (error == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error is SessionNotFoundException || error is SessionExpiredException)
|
|
||||||
{
|
|
||||||
this.SessionCache.DeleteByPrefix(address);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Statistic Statistic()
|
|
||||||
{
|
|
||||||
if (InnerPools == null)
|
|
||||||
{
|
|
||||||
throw new FrostFsInvalidObjectException(nameof(Pool));
|
|
||||||
}
|
|
||||||
|
|
||||||
var statistics = new Statistic();
|
|
||||||
|
|
||||||
foreach (var inner in InnerPools)
|
|
||||||
{
|
|
||||||
int valueIndex = 0;
|
|
||||||
var nodes = new string[inner.Clients.Length];
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
foreach (var client in inner.Clients)
|
|
||||||
{
|
|
||||||
if (client.IsHealthy())
|
|
||||||
{
|
|
||||||
nodes[valueIndex] = client.Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
var node = new NodeStatistic
|
|
||||||
{
|
|
||||||
Address = client.Address,
|
|
||||||
Methods = client.MethodsStatus(),
|
|
||||||
OverallErrors = client.GetOverallErrorRate(),
|
|
||||||
CurrentErrors = client.GetCurrentErrorRate()
|
|
||||||
};
|
|
||||||
|
|
||||||
statistics.Nodes.Add(node);
|
|
||||||
|
|
||||||
valueIndex++;
|
|
||||||
|
|
||||||
statistics.OverallErrors += node.OverallErrors;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statistics.CurrentNodes == null || statistics.CurrentNodes.Length == 0)
|
|
||||||
{
|
|
||||||
statistics.CurrentNodes = nodes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return statistics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsNetmapSnapshot> GetNetmapSnapshotAsync(CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
|
|
||||||
return await client.Client!.GetNetmapSnapshotAsync(ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsNodeInfo> GetNodeInfoAsync(CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.GetNodeInfoAsync(ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<NetworkSettings> GetNetworkSettingsAsync(CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.GetNetworkSettingsAsync(ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsSessionToken> CreateSessionAsync(PrmSessionCreate args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.CreateSessionAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ReadOnlyMemory<byte>> AddChainAsync(PrmApeChainAdd args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.AddChainAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemoveChainAsync(PrmApeChainRemove args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
await client.Client!.RemoveChainAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Chain[]> ListChainAsync(PrmApeChainList args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.ListChainAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.GetContainerAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return client.Client!.ListContainersAsync(args, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.CreateContainerAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
await client.Client!.DeleteContainerAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsHeaderResult> GetObjectHeadAsync(PrmObjectHeadGet args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.GetObjectHeadAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsObject> GetObjectAsync(PrmObjectGet args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.GetObjectAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IObjectWriter> PutObjectAsync(PrmObjectPut args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.PutObjectAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsObjectId> PutClientCutObjectAsync(PrmObjectClientCutPut args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.PutClientCutObjectAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsObjectId> PutSingleObjectAsync(PrmSingleObjectPut args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.PutSingleObjectAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsObjectId> PatchObjectAsync(PrmObjectPatch args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.PatchObjectAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<RangeReader> GetRangeAsync(PrmRangeGet args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.GetRangeAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ReadOnlyMemory<byte>[]> GetRangeHashAsync(PrmRangeHashGet args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.GetRangeHashAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FrostFsObjectId> PatchAsync(PrmObjectPatch args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.PatchObjectAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteObjectAsync(PrmObjectDelete args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
await client.Client!.DeleteObjectAsync(args, ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncEnumerable<FrostFsObjectId> SearchObjectsAsync(PrmObjectSearch args, CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return client.Client!.SearchObjectsAsync(args, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Accounting.Decimal> GetBalanceAsync(CallContext ctx)
|
|
||||||
{
|
|
||||||
var client = Connection();
|
|
||||||
return await client.Client!.GetBalanceAsync(ctx).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!disposedValue)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(disposing: true);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public class RebalanceParameters(
|
|
||||||
NodesParam[] nodesParams,
|
|
||||||
ulong nodeRequestTimeout,
|
|
||||||
ulong clientRebalanceInterval,
|
|
||||||
ulong sessionExpirationDuration)
|
|
||||||
{
|
|
||||||
public NodesParam[] NodesParams { get; set; } = nodesParams;
|
|
||||||
|
|
||||||
public ulong NodeRequestTimeout { get; set; } = nodeRequestTimeout;
|
|
||||||
|
|
||||||
public ulong ClientRebalanceInterval { get; set; } = clientRebalanceInterval;
|
|
||||||
|
|
||||||
public ulong SessionExpirationDuration { get; set; } = sessionExpirationDuration;
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
// RequestInfo groups info about pool request.
|
|
||||||
struct RequestInfo
|
|
||||||
{
|
|
||||||
public string Address { get; set; }
|
|
||||||
|
|
||||||
public MethodIndex MethodIndex { get; set; }
|
|
||||||
|
|
||||||
public TimeSpan Elapsed { get; set; }
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
internal sealed class Sampler
|
|
||||||
{
|
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
private Random random = new();
|
|
||||||
|
|
||||||
internal double[] Probabilities { get; set; }
|
|
||||||
internal int[] Alias { get; set; }
|
|
||||||
|
|
||||||
internal Sampler(double[] probabilities)
|
|
||||||
{
|
|
||||||
var small = new WorkList();
|
|
||||||
var large = new WorkList();
|
|
||||||
|
|
||||||
var n = probabilities.Length;
|
|
||||||
|
|
||||||
// sampler.randomGenerator = rand.New(source)
|
|
||||||
Probabilities = new double[n];
|
|
||||||
Alias = new int[n];
|
|
||||||
|
|
||||||
// Compute scaled probabilities.
|
|
||||||
var p = new double[n];
|
|
||||||
|
|
||||||
for (int i = 0; i < n; i++)
|
|
||||||
{
|
|
||||||
p[i] = probabilities[i] * n;
|
|
||||||
if (p[i] < 1)
|
|
||||||
small.Add(i);
|
|
||||||
else
|
|
||||||
large.Add(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (small.Length > 0 && large.Length > 0)
|
|
||||||
{
|
|
||||||
var l = small.Remove();
|
|
||||||
var g = large.Remove();
|
|
||||||
|
|
||||||
Probabilities[l] = p[l];
|
|
||||||
Alias[l] = g;
|
|
||||||
|
|
||||||
p[g] = p[g] + p[l] - 1;
|
|
||||||
|
|
||||||
if (p[g] < 1)
|
|
||||||
small.Add(g);
|
|
||||||
else
|
|
||||||
large.Add(g);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (large.Length > 0)
|
|
||||||
{
|
|
||||||
var g = large.Remove();
|
|
||||||
Probabilities[g] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (small.Length > 0)
|
|
||||||
{
|
|
||||||
var l = small.Remove();
|
|
||||||
probabilities[l] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal int Next()
|
|
||||||
{
|
|
||||||
var n = Alias.Length;
|
|
||||||
|
|
||||||
int i;
|
|
||||||
double f;
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
i = random.Next(0, n - 1);
|
|
||||||
f = random.NextDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (f < Probabilities[i])
|
|
||||||
{
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Alias[i];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public sealed class Statistic
|
|
||||||
{
|
|
||||||
public ulong OverallErrors { get; internal set; }
|
|
||||||
|
|
||||||
public Collection<NodeStatistic> Nodes { get; } = [];
|
|
||||||
|
|
||||||
public string[]? CurrentNodes { get; internal set; }
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
public class StatusSnapshot()
|
|
||||||
{
|
|
||||||
public ulong AllTime { get; internal set; }
|
|
||||||
|
|
||||||
public ulong AllRequests { get; internal set; }
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
internal sealed class WorkList
|
|
||||||
{
|
|
||||||
private readonly List<int> elements = [];
|
|
||||||
|
|
||||||
internal int Length
|
|
||||||
{
|
|
||||||
get { return elements.Count; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Add(int element)
|
|
||||||
{
|
|
||||||
elements.Add(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal int Remove()
|
|
||||||
{
|
|
||||||
int last = elements.LastOrDefault();
|
|
||||||
elements.RemoveAt(elements.Count - 1);
|
|
||||||
return last;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
|
|
||||||
using Grpc.Core;
|
|
||||||
using Grpc.Core.Interceptors;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client;
|
|
||||||
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "<Pending>")]
|
|
||||||
public struct WrapperPrm
|
|
||||||
{
|
|
||||||
internal ILogger? Logger { get; set; }
|
|
||||||
|
|
||||||
internal string Address { get; set; }
|
|
||||||
|
|
||||||
internal ECDsa Key { get; set; }
|
|
||||||
|
|
||||||
internal ulong DialTimeout { get; set; }
|
|
||||||
|
|
||||||
internal ulong StreamTimeout { get; set; }
|
|
||||||
|
|
||||||
internal uint ErrorThreshold { get; set; }
|
|
||||||
|
|
||||||
internal Action ResponseInfoCallback { get; set; }
|
|
||||||
|
|
||||||
internal Action PoolRequestInfoCallback { get; set; }
|
|
||||||
|
|
||||||
internal Func<string, ChannelBase> GrpcChannelFactory { get; set; }
|
|
||||||
|
|
||||||
internal ulong GracefulCloseOnSwitchTimeout { get; set; }
|
|
||||||
|
|
||||||
internal Action<CallStatistics>? Callback { get; set; }
|
|
||||||
|
|
||||||
internal Collection<Interceptor>? Interceptors { get; set; }
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ internal sealed class AccountingServiceProvider : ContextAccessor
|
||||||
};
|
};
|
||||||
|
|
||||||
request.AddMetaHeader([]);
|
request.AddMetaHeader([]);
|
||||||
request.Sign(ClientContext.Key.ECDsaKey);
|
request.Sign(ClientContext.Key);
|
||||||
|
|
||||||
var response = await _accountingServiceClient!.BalanceAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
var response = await _accountingServiceClient!.BalanceAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Frostfs.V2.Ape;
|
|
||||||
using Frostfs.V2.Apemanager;
|
using Frostfs.V2.Apemanager;
|
||||||
|
using Google.Protobuf;
|
||||||
|
|
||||||
namespace FrostFS.SDK.Client.Services;
|
namespace FrostFS.SDK.Client.Services;
|
||||||
|
|
||||||
|
@ -18,17 +18,19 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor
|
||||||
|
|
||||||
internal async Task<ReadOnlyMemory<byte>> AddChainAsync(PrmApeChainAdd args, CallContext ctx)
|
internal async Task<ReadOnlyMemory<byte>> AddChainAsync(PrmApeChainAdd args, CallContext ctx)
|
||||||
{
|
{
|
||||||
|
var binary = RuleSerializer.Serialize(args.Chain);
|
||||||
|
|
||||||
AddChainRequest request = new()
|
AddChainRequest request = new()
|
||||||
{
|
{
|
||||||
Body = new()
|
Body = new()
|
||||||
{
|
{
|
||||||
Chain = new() { Raw = args.Chain.GetRaw() },
|
Chain = new() { Raw = UnsafeByteOperations.UnsafeWrap(binary) },
|
||||||
Target = args.Target.GetChainTarget()
|
Target = args.Target.GetChainTarget()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
request.AddMetaHeader(args.XHeaders);
|
request.AddMetaHeader(args.XHeaders);
|
||||||
request.Sign(ClientContext.Key.ECDsaKey);
|
request.Sign(ClientContext.Key);
|
||||||
|
|
||||||
var response = await _apeManagerServiceClient!.AddChainAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
var response = await _apeManagerServiceClient!.AddChainAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
||||||
|
|
||||||
|
@ -43,20 +45,20 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor
|
||||||
{
|
{
|
||||||
Body = new()
|
Body = new()
|
||||||
{
|
{
|
||||||
ChainId = args.Chain.GetRaw(),
|
ChainId = UnsafeByteOperations.UnsafeWrap(args.ChainId),
|
||||||
Target = args.Target.GetChainTarget()
|
Target = args.Target.GetChainTarget()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
request.AddMetaHeader(args.XHeaders);
|
request.AddMetaHeader(args.XHeaders);
|
||||||
request.Sign(ClientContext.Key.ECDsaKey);
|
request.Sign(ClientContext.Key);
|
||||||
|
|
||||||
var response = await _apeManagerServiceClient!.RemoveChainAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
var response = await _apeManagerServiceClient!.RemoveChainAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
||||||
|
|
||||||
Verifier.CheckResponse(response);
|
Verifier.CheckResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<Chain[]> ListChainAsync(PrmApeChainList args, CallContext ctx)
|
internal async Task<FrostFsChain[]> ListChainAsync(PrmApeChainList args, CallContext ctx)
|
||||||
{
|
{
|
||||||
ListChainsRequest request = new()
|
ListChainsRequest request = new()
|
||||||
{
|
{
|
||||||
|
@ -67,12 +69,12 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor
|
||||||
};
|
};
|
||||||
|
|
||||||
request.AddMetaHeader(args.XHeaders);
|
request.AddMetaHeader(args.XHeaders);
|
||||||
request.Sign(ClientContext.Key.ECDsaKey);
|
request.Sign(ClientContext.Key);
|
||||||
|
|
||||||
var response = await _apeManagerServiceClient!.ListChainsAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
var response = await _apeManagerServiceClient!.ListChainsAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
||||||
|
|
||||||
Verifier.CheckResponse(response);
|
Verifier.CheckResponse(response);
|
||||||
|
|
||||||
return [.. response.Body.Chains];
|
return [.. response.Body.Chains.Select(c => RuleSerializer.Deserialize([.. c.Raw]))];
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using FrostFS.Container;
|
using FrostFS.Container;
|
||||||
|
@ -39,7 +38,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
|
||||||
|
|
||||||
internal async Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args, CallContext ctx)
|
internal async Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args, CallContext ctx)
|
||||||
{
|
{
|
||||||
GetRequest request = GetContainerRequest(args.Container.ContainerID, args.XHeaders, ClientContext.Key.ECDsaKey);
|
GetRequest request = GetContainerRequest(args.Container.GetContainerID(), args.XHeaders, ClientContext.Key);
|
||||||
|
|
||||||
var response = await service.GetAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
var response = await service.GetAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
||||||
|
|
||||||
|
@ -59,7 +58,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
|
||||||
};
|
};
|
||||||
|
|
||||||
request.AddMetaHeader(args.XHeaders);
|
request.AddMetaHeader(args.XHeaders);
|
||||||
request.Sign(ClientContext.Key.ECDsaKey);
|
request.Sign(ClientContext.Key);
|
||||||
|
|
||||||
var response = await service.ListAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
var response = await service.ListAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
||||||
|
|
||||||
|
@ -71,7 +70,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args, CallContext ctx)
|
internal async Task<FrostFsContainerId> PutContainerAsync(PrmContainerCreate args, CallContext ctx)
|
||||||
{
|
{
|
||||||
var grpcContainer = args.Container.GetContainer();
|
var grpcContainer = args.Container.GetContainer();
|
||||||
|
|
||||||
|
@ -83,7 +82,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
|
||||||
Body = new PutRequest.Types.Body
|
Body = new PutRequest.Types.Body
|
||||||
{
|
{
|
||||||
Container = grpcContainer,
|
Container = grpcContainer,
|
||||||
Signature = ClientContext.Key.ECDsaKey.SignRFC6979(grpcContainer)
|
Signature = ClientContext.Key.SignRFC6979(grpcContainer)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -96,7 +95,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
|
||||||
|
|
||||||
request.AddMetaHeader(args.XHeaders, protoToken);
|
request.AddMetaHeader(args.XHeaders, protoToken);
|
||||||
|
|
||||||
request.Sign(ClientContext.Key.ECDsaKey);
|
request.Sign(ClientContext.Key);
|
||||||
|
|
||||||
var response = await service.PutAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
var response = await service.PutAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
||||||
|
|
||||||
|
@ -113,8 +112,8 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
|
||||||
{
|
{
|
||||||
Body = new DeleteRequest.Types.Body
|
Body = new DeleteRequest.Types.Body
|
||||||
{
|
{
|
||||||
ContainerId = args.ContainerId.ToMessage(),
|
ContainerId = args.ContainerId.GetContainerID(),
|
||||||
Signature = ClientContext.Key.ECDsaKey.SignRFC6979(args.ContainerId.ToMessage().Value)
|
Signature = ClientContext.Key.SignRFC6979(args.ContainerId.GetContainerID().Value)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -127,7 +126,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
|
||||||
|
|
||||||
request.AddMetaHeader(args.XHeaders, protoToken);
|
request.AddMetaHeader(args.XHeaders, protoToken);
|
||||||
|
|
||||||
request.Sign(ClientContext.Key.ECDsaKey);
|
request.Sign(ClientContext.Key);
|
||||||
|
|
||||||
var response = await service.DeleteAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
var response = await service.DeleteAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
||||||
|
|
||||||
|
@ -139,7 +138,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
|
||||||
Verifier.CheckResponse(response);
|
Verifier.CheckResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GetRequest GetContainerRequest(ContainerID id, string[] xHeaders, ECDsa key)
|
private static GetRequest GetContainerRequest(ContainerID id, string[] xHeaders, ClientKey key)
|
||||||
{
|
{
|
||||||
var request = new GetRequest
|
var request = new GetRequest
|
||||||
{
|
{
|
||||||
|
@ -163,7 +162,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
|
||||||
|
|
||||||
private async Task WaitForContainer(WaitExpects expect, ContainerID id, PrmWait waitParams, CallContext ctx)
|
private async Task WaitForContainer(WaitExpects expect, ContainerID id, PrmWait waitParams, CallContext ctx)
|
||||||
{
|
{
|
||||||
var request = GetContainerRequest(id, [], ClientContext.Key.ECDsaKey);
|
var request = GetContainerRequest(id, [], ClientContext.Key);
|
||||||
|
|
||||||
async Task action()
|
async Task action()
|
||||||
{
|
{
|
||||||
|
|
|
@ -50,7 +50,7 @@ internal sealed class NetmapServiceProvider : ContextAccessor
|
||||||
};
|
};
|
||||||
|
|
||||||
request.AddMetaHeader([]);
|
request.AddMetaHeader([]);
|
||||||
request.Sign(ClientContext.Key.ECDsaKey);
|
request.Sign(ClientContext.Key);
|
||||||
|
|
||||||
var response = await netmapServiceClient.LocalNodeInfoAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
var response = await netmapServiceClient.LocalNodeInfoAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ internal sealed class NetmapServiceProvider : ContextAccessor
|
||||||
var request = new NetworkInfoRequest();
|
var request = new NetworkInfoRequest();
|
||||||
|
|
||||||
request.AddMetaHeader([]);
|
request.AddMetaHeader([]);
|
||||||
request.Sign(ClientContext.Key.ECDsaKey);
|
request.Sign(ClientContext.Key);
|
||||||
|
|
||||||
var response = await netmapServiceClient.NetworkInfoAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken)
|
var response = await netmapServiceClient.NetworkInfoAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
@ -79,7 +79,7 @@ internal sealed class NetmapServiceProvider : ContextAccessor
|
||||||
var request = new NetmapSnapshotRequest();
|
var request = new NetmapSnapshotRequest();
|
||||||
|
|
||||||
request.AddMetaHeader([]);
|
request.AddMetaHeader([]);
|
||||||
request.Sign(ClientContext.Key.ECDsaKey);
|
request.Sign(ClientContext.Key);
|
||||||
|
|
||||||
var response = await netmapServiceClient.NetmapSnapshotAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
var response = await netmapServiceClient.NetmapSnapshotAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken);
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue