Compare commits
184 Commits
Author | SHA1 | Date | |
---|---|---|---|
c99d0d4b25 | |||
d78216f528 | |||
c05c027a24 | |||
3fd64f913a | |||
f935290bbc | |||
ca91f898a2 | |||
fcbada7798 | |||
fad9bdc3e1 | |||
198ccb8b7b | |||
dc5d5c6ac8 | |||
f771eaca47 | |||
2d1e2e8e64 | |||
6412758177 | |||
836c8159f6 | |||
e406e6e8f4 | |||
2fa2c6284e | |||
2862c4fa12 | |||
6f89fbf8b5 | |||
6ae7ec9a3f | |||
4a35b1b20a | |||
c859c97ee2 | |||
a091c629e1 | |||
96de94a584 | |||
e9cd8410d7 | |||
e37ede1d2e | |||
4420a29ac4 | |||
0544d4bfd0 | |||
fe7379f102 | |||
c76df5052b | |||
3299cad1c3 | |||
d9ab018c49 | |||
e853451cd2 | |||
1becf9d2f5 | |||
1a712cf187 | |||
023f335f67 | |||
bf0da78b63 | |||
e8473850a2 | |||
b836d187fd | |||
9b09229c4d | |||
546c0f7ed6 | |||
adbad1c9b5 | |||
273b986751 | |||
5b205729b9 | |||
fe900b09dd | |||
494c012659 | |||
4abc381ebe | |||
73c8fdac53 | |||
ee2717493a | |||
2435eb9ecd | |||
8fb533dabe | |||
2f0f5ac504 | |||
9ab811d478 | |||
e0a99fb4ba | |||
d40982fc91 | |||
fe3a1cc31b | |||
70713706a1 | |||
0054e7e89b | |||
97f718b504 | |||
202da9270e | |||
6e83ec0ed7 | |||
5c44cdfdaa | |||
09a239f040 | |||
3faff8b2e2 | |||
2345fda18e | |||
5695120efc | |||
183293e061 | |||
4b48876f0e | |||
5089bf58fb | |||
480a347179 | |||
59e560c7a7 | |||
0bd9bea2e9 | |||
bd7581ac59 | |||
db378c3d26 | |||
23740162dc | |||
96422a955f | |||
6fd996fdac | |||
9efa00d103 | |||
72d30f4c34 | |||
2e92779777 | |||
404415b1e3 | |||
07e421d245 | |||
a7d6e29275 | |||
1a8b295dab | |||
ffc45cc066 | |||
0db1ba8093 | |||
43f7c94ac8 | |||
93d13fb5b4 | |||
6a1e3e73dd | |||
ec576ee5ac | |||
606d79afc4 | |||
f4d15a430c | |||
4a841459f1 | |||
ee8c577fc0 | |||
8ae0f94cd7 | |||
69a97863a9 | |||
12c7e4a9f8 | |||
23cced240b | |||
e73c928d85 | |||
779ad90f9a | |||
dca1740be5 | |||
487b34d857 | |||
a31283cf51 | |||
b722bedf8a | |||
d53923c636 | |||
9356665d60 | |||
0932d17395 | |||
2a3ea3f996 | |||
e5a5e5f7c6 | |||
00bdd907d5 | |||
8eab756d3f | |||
3d9b1d1635 | |||
4218193dd7 | |||
6499d01c9b | |||
83b39b4f6b | |||
21092ca715 | |||
a4e79d7ebf | |||
846883a979 | |||
c7a3edb90f | |||
f308a27e91 | |||
1d37154793 | |||
092d069d3e | |||
ab5c4e23bd | |||
59bf6693c7 | |||
affcbfbf06 | |||
e81df2648c | |||
27a450235a | |||
42454f9ed8 | |||
7ea8860670 | |||
2fb72029ef | |||
77af59796d | |||
b732f96e07 | |||
602198105d | |||
e513cbd562 | |||
4198369dd0 | |||
debecc1868 | |||
140fc04c62 | |||
7e34665774 | |||
be541f3641 | |||
e582416994 | |||
842145ecb3 | |||
d68936c4da | |||
24a90baff8 | |||
6b7891d5f1 | |||
129b271ff8 | |||
a11ee983c4 | |||
bec58d5f58 | |||
4b6f9b79e6 | |||
f7ec7f025b | |||
34c76a47c1 | |||
525653ff51 | |||
a647b79038 | |||
9bc1d08753 | |||
6a79bda691 | |||
1edfcd6859 | |||
f51fdbccec | |||
faeeb2fc75 | |||
d50c487132 | |||
b837feffe4 | |||
4d89640195 | |||
1292d453c3 | |||
ec20b381ed | |||
37cc3f5262 | |||
7f1940e5ed | |||
caccf8e5e6 | |||
ef65dfe2eb | |||
ff6c6916f2 | |||
3dfe8765d3 | |||
a4a52cb15d | |||
014970930a | |||
4628be982c | |||
ff55e5a188 | |||
bf0898266c | |||
b9d69f7698 | |||
6f48bda7ac | |||
316534e09e | |||
3cecbdb464 | |||
62f11e43ee | |||
064c1585ee | |||
15300a1eb8 | |||
58dd047ee4 | |||
4b42ea6cd7 | |||
53c27ae621 | |||
269de67bde | |||
8bbccf1047 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -5,4 +5,4 @@ A good bug report has some very specific qualities, so please read over our shor
|
||||
|
||||
To ask a question, go ahead and ignore this.
|
||||
|
||||
[report_bugs]: https://github.com/coreos/etcd/blob/master/Documentation/reporting_bugs.md
|
||||
[report_bugs]: ../Documentation/reporting_bugs.md
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -2,4 +2,4 @@
|
||||
|
||||
Please read our [contribution workflow][contributing] before submitting a pull request.
|
||||
|
||||
[contributing]: https://github.com/coreos/etcd/blob/master/CONTRIBUTING.md#contribution-flow
|
||||
[contributing]: ../CONTRIBUTING.md#contribution-flow
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
/coverage
|
||||
/gopath
|
||||
/gopath.proto
|
||||
/go-bindata
|
||||
/machine*
|
||||
/bin
|
||||
|
33
.travis.yml
33
.travis.yml
@ -4,18 +4,16 @@ go_import_path: github.com/coreos/etcd
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.7.5
|
||||
|
||||
notifications:
|
||||
on_success: never
|
||||
on_failure: never
|
||||
- 1.6
|
||||
- tip
|
||||
|
||||
env:
|
||||
global:
|
||||
- GO15VENDOREXPERIMENT=1
|
||||
matrix:
|
||||
- TARGET=amd64
|
||||
- TARGET=arm64
|
||||
- TARGET=arm
|
||||
- TARGET=386
|
||||
- TARGET=ppc64le
|
||||
|
||||
matrix:
|
||||
@ -23,12 +21,12 @@ matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
exclude:
|
||||
- go: 1.6
|
||||
env: TARGET=arm64
|
||||
- go: tip
|
||||
env: TARGET=arm
|
||||
- go: tip
|
||||
env: TARGET=arm64
|
||||
- go: tip
|
||||
env: TARGET=386
|
||||
- go: tip
|
||||
env: TARGET=ppc64le
|
||||
|
||||
@ -46,19 +44,12 @@ before_install:
|
||||
|
||||
# disable godep restore override
|
||||
install:
|
||||
- pushd cmd/etcd && go get -t -v ./... && popd
|
||||
- pushd cmd/ && go get -t -v ./... && popd
|
||||
|
||||
script:
|
||||
- >
|
||||
case "${TARGET}" in
|
||||
amd64)
|
||||
GOARCH=amd64 ./test
|
||||
;;
|
||||
386)
|
||||
GOARCH=386 PASSES="build unit" ./test
|
||||
;;
|
||||
*)
|
||||
# test building out of gopath
|
||||
GO_BUILD_FLAGS="-a -v" GOPATH="" GOARCH="${TARGET}" ./build
|
||||
;;
|
||||
esac
|
||||
if [ "${TARGET}" == "amd64" ]; then
|
||||
GOARCH="${TARGET}" ./test;
|
||||
else
|
||||
GOARCH="${TARGET}" ./build;
|
||||
fi
|
||||
|
@ -1 +0,0 @@
|
||||
docs.md
|
@ -14,7 +14,7 @@ GCE n1-highcpu-2 machine type
|
||||
|
||||
## Testing
|
||||
|
||||
Bootstrap another machine and use the [hey HTTP benchmark tool][hey] to send requests to each etcd member. Check the [benchmark hacking guide][hack-benchmark] for detailed instructions.
|
||||
Bootstrap another machine and use the [boom HTTP benchmark tool][boom] to send requests to each etcd member. Check the [benchmark hacking guide][hack-benchmark] for detailed instructions.
|
||||
|
||||
## Performance
|
||||
|
||||
@ -48,5 +48,5 @@ Bootstrap another machine and use the [hey HTTP benchmark tool][hey] to send req
|
||||
| 256 | 64 | all servers | 1033 | 121.5 |
|
||||
| 256 | 256 | all servers | 3061 | 119.3 |
|
||||
|
||||
[hey]: https://github.com/rakyll/hey
|
||||
[boom]: https://github.com/rakyll/boom
|
||||
[hack-benchmark]: /hack/benchmark/
|
||||
|
@ -24,7 +24,7 @@ Go OS/Arch: linux/amd64
|
||||
|
||||
## Testing
|
||||
|
||||
Bootstrap another machine, outside of the etcd cluster, and run the [`hey` HTTP benchmark tool](https://github.com/rakyll/hey) with a connection reuse patch to send requests to each etcd cluster member. See the [benchmark instructions](../../hack/benchmark/) for the patch and the steps to reproduce our procedures.
|
||||
Bootstrap another machine, outside of the etcd cluster, and run the [`boom` HTTP benchmark tool](https://github.com/rakyll/boom) with a connection reuse patch to send requests to each etcd cluster member. See the [benchmark instructions](../../hack/benchmark/) for the patch and the steps to reproduce our procedures.
|
||||
|
||||
The performance is calulated through results of 100 benchmark rounds.
|
||||
|
||||
@ -66,4 +66,4 @@ The performance is calulated through results of 100 benchmark rounds.
|
||||
|
||||
- Write QPS to cluster leaders seems to be increased by a small margin. This is because the main loop and entry apply loops were decoupled in the etcd raft logic, eliminating several blocks between them.
|
||||
|
||||
- Write QPS to all members seems to be increased by a significant margin, because followers now receive the latest commit index sooner, and commit proposals more quickly.
|
||||
- Write QPS to all members seems to be increased by a significant margin, because followers now receive the latest commit index sooner, and commit proposals more quickly.
|
@ -24,7 +24,7 @@ Also, we use 3 etcd 2.1.0 alpha-stage members to form cluster to get base perfor
|
||||
|
||||
## Testing
|
||||
|
||||
Bootstrap another machine and use the [hey HTTP benchmark tool][hey] to send requests to each etcd member. Check the [benchmark hacking guide][hack-benchmark] for detailed instructions.
|
||||
Bootstrap another machine and use the [boom HTTP benchmark tool][boom] to send requests to each etcd member. Check the [benchmark hacking guide][hack-benchmark] for detailed instructions.
|
||||
|
||||
## Performance
|
||||
|
||||
@ -66,7 +66,7 @@ Bootstrap another machine and use the [hey HTTP benchmark tool][hey] to send req
|
||||
|
||||
- write QPS to all servers is increased by 30~80% because follower could receive latest commit index earlier and commit proposals faster.
|
||||
|
||||
[hey]: https://github.com/rakyll/hey
|
||||
[boom]: https://github.com/rakyll/boom
|
||||
[c7146bd5]: https://github.com/coreos/etcd/commits/c7146bd5f2c73716091262edc638401bb8229144
|
||||
[etcd-2.1-benchmark]: etcd-2-1-0-alpha-benchmarks.md
|
||||
[hack-benchmark]: /hack/benchmark/
|
||||
|
@ -8,8 +8,6 @@ etcd v3 uses [gRPC][grpc] for its messaging protocol. The etcd project includes
|
||||
|
||||
The gateway accepts a [JSON mapping][json-mapping] for etcd's [protocol buffer][api-ref] message definitions. Note that `key` and `value` fields are defined as byte arrays and therefore must be base64 encoded in JSON.
|
||||
|
||||
Use `curl` to put and get a key:
|
||||
|
||||
```bash
|
||||
<<COMMENT
|
||||
https://www.base64encode.org/
|
||||
@ -19,24 +17,11 @@ COMMENT
|
||||
|
||||
curl -L http://localhost:2379/v3alpha/kv/put \
|
||||
-X POST -d '{"key": "Zm9v", "value": "YmFy"}'
|
||||
# {"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"2","raft_term":"3"}}
|
||||
|
||||
curl -L http://localhost:2379/v3alpha/kv/range \
|
||||
-X POST -d '{"key": "Zm9v"}'
|
||||
# {"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"2","raft_term":"3"},"kvs":[{"key":"Zm9v","create_revision":"2","mod_revision":"2","version":"1","value":"YmFy"}],"count":"1"}
|
||||
```
|
||||
|
||||
Use `curl` to watch a key:
|
||||
|
||||
```bash
|
||||
curl http://localhost:2379/v3alpha/watch \
|
||||
-X POST -d '{"create_request": {"key":"Zm9v"} }' &
|
||||
# {"result":{"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"1","raft_term":"2"},"created":true}}
|
||||
|
||||
curl -L http://localhost:2379/v3alpha/kv/put \
|
||||
-X POST -d '{"key": "Zm9v", "value": "YmFy"}' >/dev/null 2>&1
|
||||
# {"result":{"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"2","raft_term":"2"},"events":[{"kv":{"key":"Zm9v","create_revision":"2","mod_revision":"2","version":"1","value":"YmFy"}}]}}
|
||||
```
|
||||
|
||||
## Swagger
|
||||
|
||||
|
@ -59,7 +59,6 @@ for grpc-gateway
|
||||
| LeaseGrant | LeaseGrantRequest | LeaseGrantResponse | LeaseGrant creates a lease which expires if the server does not receive a keepAlive within a given time to live period. All keys attached to the lease will be expired and deleted if the lease expires. Each expired key generates a delete event in the event history. |
|
||||
| LeaseRevoke | LeaseRevokeRequest | LeaseRevokeResponse | LeaseRevoke revokes a lease. All keys attached to the lease will expire and be deleted. |
|
||||
| LeaseKeepAlive | LeaseKeepAliveRequest | LeaseKeepAliveResponse | LeaseKeepAlive keeps the lease alive by streaming keep alive requests from the client to the server and streaming keep alive responses from the server to the client. |
|
||||
| LeaseTimeToLive | LeaseTimeToLiveRequest | LeaseTimeToLiveResponse | LeaseTimeToLive retrieves lease information. |
|
||||
|
||||
|
||||
|
||||
@ -427,7 +426,7 @@ Empty field.
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| key | key is the first key to delete in the range. | bytes |
|
||||
| range_end | range_end is the key following the last key to delete for the range [key, range_end). If range_end is not given, the range is defined to contain only the key argument. If range_end is one bit larger than the given key, then the range is all the all keys with the prefix (the given key). If range_end is '\0', the range is all keys greater than or equal to the key argument. | bytes |
|
||||
| range_end | range_end is the key following the last key to delete for the range [key, range_end). If range_end is not given, the range is defined to contain only the key argument. If range_end is '\0', the range is all keys greater than or equal to the key argument. | bytes |
|
||||
| prev_kv | If prev_kv is set, etcd gets the previous key-value pairs before deleting it. The previous key-value pairs will be returned in the delte response. | bool |
|
||||
|
||||
|
||||
@ -511,27 +510,6 @@ Empty field.
|
||||
|
||||
|
||||
|
||||
##### message `LeaseTimeToLiveRequest` (etcdserver/etcdserverpb/rpc.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| ID | ID is the lease ID for the lease. | int64 |
|
||||
| keys | keys is true to query all the keys attached to this lease. | bool |
|
||||
|
||||
|
||||
|
||||
##### message `LeaseTimeToLiveResponse` (etcdserver/etcdserverpb/rpc.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| header | | ResponseHeader |
|
||||
| ID | ID is the lease ID from the keep alive request. | int64 |
|
||||
| TTL | TTL is the remaining TTL in seconds for the lease; the lease will expire in under TTL+1 seconds. | int64 |
|
||||
| grantedTTL | GrantedTTL is the initial granted time in seconds upon lease creation/renewal. | int64 |
|
||||
| keys | Keys is the list of keys attached to this lease. | (slice of) bytes |
|
||||
|
||||
|
||||
|
||||
##### message `Member` (etcdserver/etcdserverpb/rpc.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
@ -641,10 +619,6 @@ Empty field.
|
||||
| serializable | serializable sets the range request to use serializable member-local reads. Range requests are linearizable by default; linearizable requests have higher latency and lower throughput than serializable requests but reflect the current consensus of the cluster. For better performance, in exchange for possible stale reads, a serializable range request is served locally without needing to reach consensus with other nodes in the cluster. | bool |
|
||||
| keys_only | keys_only when set returns only the keys and not the values. | bool |
|
||||
| count_only | count_only when set returns only the count of the keys in the range. | bool |
|
||||
| min_mod_revision | min_mod_revision is the lower bound for returned key mod revisions; all keys with lesser mod revisions will be filtered away. | int64 |
|
||||
| max_mod_revision | max_mod_revision is the upper bound for returned key mod revisions; all keys with greater mod revisions will be filtered away. | int64 |
|
||||
| min_create_revision | min_create_revision is the lower bound for returned key create revisions; all keys with lesser create trevisions will be filtered away. | int64 |
|
||||
| max_create_revision | max_create_revision is the upper bound for returned key create revisions; all keys with greater create revisions will be filtered away. | int64 |
|
||||
|
||||
|
||||
|
||||
@ -762,10 +736,9 @@ From google paxosdb paper: Our implementation hinges around a powerful primitive
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| key | key is the key to register for watching. | bytes |
|
||||
| range_end | range_end is the end of the range [key, range_end) to watch. If range_end is not given, only the key argument is watched. If range_end is equal to '\0', all keys greater than or equal to the key argument are watched. If the range_end is one bit larger than the given key, then all keys with the prefix (the given key) will be watched. | bytes |
|
||||
| range_end | range_end is the end of the range [key, range_end) to watch. If range_end is not given, only the key argument is watched. If range_end is equal to '\0', all keys greater than or equal to the key argument are watched. | bytes |
|
||||
| start_revision | start_revision is an optional revision to watch from (inclusive). No start_revision is "now". | int64 |
|
||||
| progress_notify | progress_notify is set so that the etcd server will periodically send a WatchResponse with no events to the new watcher if there are no recent events. It is useful when clients wish to recover a disconnected watcher starting from a recent known revision. The etcd server may decide how often it will send notifications based on current load. | bool |
|
||||
| filters | filter out put event. filter out delete event. filters filter the events at server side before it sends back to the watcher. | (slice of) FilterType |
|
||||
| prev_kv | If prev_kv is set, created watcher gets the previous KV before the event happens. If the previous KV is already compacted, nothing will be returned. | bool |
|
||||
|
||||
|
||||
@ -825,22 +798,6 @@ From google paxosdb paper: Our implementation hinges around a powerful primitive
|
||||
|
||||
|
||||
|
||||
##### message `LeaseInternalRequest` (lease/leasepb/lease.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| LeaseTimeToLiveRequest | | etcdserverpb.LeaseTimeToLiveRequest |
|
||||
|
||||
|
||||
|
||||
##### message `LeaseInternalResponse` (lease/leasepb/lease.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| LeaseTimeToLiveResponse | | etcdserverpb.LeaseTimeToLiveResponse |
|
||||
|
||||
|
||||
|
||||
##### message `Permission` (auth/authpb/auth.proto)
|
||||
|
||||
Permission is a single entity
|
||||
|
@ -636,33 +636,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v3alpha/kv/lease/timetolive": {
|
||||
"post": {
|
||||
"summary": "LeaseTimeToLive retrieves lease information.",
|
||||
"operationId": "LeaseTimeToLive",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/etcdserverpbLeaseTimeToLiveResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/etcdserverpbLeaseTimeToLiveRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Lease"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v3alpha/kv/put": {
|
||||
"post": {
|
||||
"summary": "Put puts the given key into the key-value store.\nA put request increments the revision of the key-value store\nand generates one event in the event history.",
|
||||
@ -978,8 +951,7 @@
|
||||
"enum": [
|
||||
"EQUAL",
|
||||
"GREATER",
|
||||
"LESS",
|
||||
"NOT_EQUAL"
|
||||
"LESS"
|
||||
],
|
||||
"default": "EQUAL"
|
||||
},
|
||||
@ -1021,15 +993,6 @@
|
||||
],
|
||||
"default": "KEY"
|
||||
},
|
||||
"WatchCreateRequestFilterType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"NOPUT",
|
||||
"NODELETE"
|
||||
],
|
||||
"default": "NOPUT",
|
||||
"description": "- NOPUT: filter out put event.\n - NODELETE: filter out delete event."
|
||||
},
|
||||
"authpbPermission": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -1519,7 +1482,7 @@
|
||||
"range_end": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "range_end is the key following the last key to delete for the range [key, range_end).\nIf range_end is not given, the range is defined to contain only the key argument.\nIf range_end is one bit larger than the given key, then the range is all\nthe all keys with the prefix (the given key).\nIf range_end is '\\0', the range is all keys greater than or equal to the key argument."
|
||||
"description": "range_end is the key following the last key to delete for the range [key, range_end).\nIf range_end is not given, the range is defined to contain only the key argument.\nIf range_end is '\\0', the range is all keys greater than or equal to the key argument."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1642,52 +1605,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"etcdserverpbLeaseTimeToLiveRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ID": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "ID is the lease ID for the lease."
|
||||
},
|
||||
"keys": {
|
||||
"type": "boolean",
|
||||
"format": "boolean",
|
||||
"description": "keys is true to query all the keys attached to this lease."
|
||||
}
|
||||
}
|
||||
},
|
||||
"etcdserverpbLeaseTimeToLiveResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ID": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "ID is the lease ID from the keep alive request."
|
||||
},
|
||||
"TTL": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "TTL is the remaining TTL in seconds for the lease; the lease will expire in under TTL+1 seconds."
|
||||
},
|
||||
"grantedTTL": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "GrantedTTL is the initial granted time in seconds upon lease creation/renewal."
|
||||
},
|
||||
"header": {
|
||||
"$ref": "#/definitions/etcdserverpbResponseHeader"
|
||||
},
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
"description": "Keys is the list of keys attached to this lease."
|
||||
}
|
||||
}
|
||||
},
|
||||
"etcdserverpbMember": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -1866,26 +1783,6 @@
|
||||
"format": "int64",
|
||||
"description": "limit is a limit on the number of keys returned for the request."
|
||||
},
|
||||
"max_create_revision": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "max_create_revision is the upper bound for returned key create revisions; all keys with\ngreater create revisions will be filtered away."
|
||||
},
|
||||
"max_mod_revision": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "max_mod_revision is the upper bound for returned key mod revisions; all keys with\ngreater mod revisions will be filtered away."
|
||||
},
|
||||
"min_create_revision": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "min_create_revision is the lower bound for returned key create revisions; all keys with\nlesser create trevisions will be filtered away."
|
||||
},
|
||||
"min_mod_revision": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "min_mod_revision is the lower bound for returned key mod revisions; all keys with\nlesser mod revisions will be filtered away."
|
||||
},
|
||||
"range_end": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
@ -2107,13 +2004,6 @@
|
||||
"etcdserverpbWatchCreateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/WatchCreateRequestFilterType"
|
||||
},
|
||||
"description": "filters filter the events at server side before it sends back to the watcher."
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
@ -2132,7 +2022,7 @@
|
||||
"range_end": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "range_end is the end of the range [key, range_end) to watch. If range_end is not given,\nonly the key argument is watched. If range_end is equal to '\\0', all keys greater than\nor equal to the key argument are watched.\nIf the range_end is one bit larger than the given key,\nthen all keys with the prefix (the given key) will be watched."
|
||||
"description": "range_end is the end of the range [key, range_end) to watch. If range_end is not given,\nonly the key argument is watched. If range_end is equal to '\\0', all keys greater than\nor equal to the key argument are watched."
|
||||
},
|
||||
"start_revision": {
|
||||
"type": "string",
|
||||
|
@ -1,11 +1,8 @@
|
||||
# Experimental APIs and features
|
||||
|
||||
For the most part, the etcd project is stable, but we are still moving fast! We believe in the release fast philosophy. We want to get early feedback on features still in development and stabilizing. Thus, there are, and will be more, experimental features and APIs. We plan to improve these features based on the early feedback from the community, or abandon them if there is little interest, in the next few releases. Please do not rely on any experimental features or APIs in production environment.
|
||||
For the most part, the etcd project is stable, but we are still moving fast! We believe in the release fast philosophy. We want to get early feedback on features still in development and stabilizing. Thus, there are, and will be more, experimental features and APIs. We plan to improve these features based on the early feedback from the community, or abandon them if there is little interest, in the next few releases. If you are running a production system, please do not rely on any experimental features or APIs.
|
||||
|
||||
## The current experimental API/features are:
|
||||
|
||||
- [gateway][gateway]: beta, to be stable in 3.2 release
|
||||
- [gRPC proxy][grpc-proxy]: alpha, to be stable in 3.2 release
|
||||
|
||||
[gateway]: ../op-guide/gateway.md
|
||||
[grpc-proxy]: ../op-guide/grpc_proxy.md
|
||||
- v3 auth API: expect to be stable in 3.1 release
|
||||
- etcd gateway: expect to be stable in 3.1 release
|
||||
|
@ -1,65 +0,0 @@
|
||||
# gRPC naming and discovery
|
||||
|
||||
etcd provides a gRPC resolver to support an alternative name system that fetches endpoints from etcd for discovering gRPC services. The underlying mechanism is based on watching updates to keys prefixed with the service name.
|
||||
|
||||
## Using etcd discovery with go-grpc
|
||||
|
||||
The etcd client provides a gRPC resolver for resolving gRPC endpoints with an etcd backend. The resolver is initialized with an etcd client and given a target for resolution:
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
etcdnaming "github.com/coreos/etcd/clientv3/naming"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
...
|
||||
|
||||
cli, cerr := clientv3.NewFromURL("http://localhost:2379")
|
||||
r := &etcdnaming.GRPCResolver{Client: cli}
|
||||
b := grpc.RoundRobin(r)
|
||||
conn, gerr := grpc.Dial("my-service", grpc.WithBalancer(b))
|
||||
```
|
||||
|
||||
## Managing service endpoints
|
||||
|
||||
The etcd resolver treats all keys under the prefix of the resolution target following a "/" (e.g., "my-service/") with JSON-encoded go-grpc `naming.Update` values as potential service endpoints. Endpoints are added to the service by creating new keys and removed from the service by deleting keys.
|
||||
|
||||
### Adding an endpoint
|
||||
|
||||
New endpoints can be added to the service through `etcdctl`:
|
||||
|
||||
```sh
|
||||
ETCDCTL_API=3 etcdctl put my-service/1.2.3.4 '{"Addr":"1.2.3.4","Metadata":"..."}'
|
||||
```
|
||||
|
||||
The etcd client's `GRPCResolver.Update` method can also register new endpoints with a key matching the `Addr`:
|
||||
|
||||
```go
|
||||
r.Update(context.TODO(), "my-service", naming.Update{Op: naming.Add, Addr: "1.2.3.4", Metadata: "..."})
|
||||
```
|
||||
|
||||
### Deleting an endpoint
|
||||
|
||||
Hosts can be deleted from the service through `etcdctl`:
|
||||
|
||||
```sh
|
||||
ETCDCTL_API=3 etcdctl del my-service/1.2.3.4
|
||||
```
|
||||
|
||||
The etcd client's `GRPCResolver.Update` method also supports deleting endpoints:
|
||||
|
||||
```go
|
||||
r.Update(context.TODO(), "my-service", naming.Update{Op: naming.Delete, Addr: "1.2.3.4"})
|
||||
```
|
||||
|
||||
### Registering an endpoint with a lease
|
||||
|
||||
Registering an endpoint with a lease ensures that if the host can't maintain a keepalive heartbeat (e.g., its machine fails), it will be removed from the service:
|
||||
|
||||
```sh
|
||||
lease=`ETCDCTL_API=3 etcdctl lease grant 5 | cut -f2 -d' '`
|
||||
ETCDCTL_API=3 etcdctl put --lease=$lease my-service/1.2.3.4 '{"Addr":"1.2.3.4","Metadata":"..."}'
|
||||
ETCDCTL_API=3 etcdctl lease keep-alive $lease
|
||||
```
|
@ -4,54 +4,30 @@ Users mostly interact with etcd by putting or getting the value of a key. This s
|
||||
|
||||
By default, etcdctl talks to the etcd server with the v2 API for backward compatibility. For etcdctl to speak to etcd using the v3 API, the API version must be set to version 3 via the `ETCDCTL_API` environment variable.
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
export ETCDCTL_API=3
|
||||
```
|
||||
|
||||
## Find versions
|
||||
|
||||
etcdctl version and Server API version can be useful in finding the appropriate commands to be used for performing various opertions on etcd.
|
||||
|
||||
Here is the command to find the versions:
|
||||
|
||||
```bash
|
||||
$ etcdctl version
|
||||
etcdctl version: 3.1.0-alpha.0+git
|
||||
API version: 3.1
|
||||
```
|
||||
|
||||
## Write a key
|
||||
|
||||
Applications store keys into the etcd cluster by writing to keys. Every stored key is replicated to all etcd cluster members through the Raft protocol to achieve consistency and reliability.
|
||||
|
||||
Here is the command to set the value of key `foo` to `bar`:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
$ etcdctl put foo bar
|
||||
OK
|
||||
```
|
||||
|
||||
Also a key can be set for a specified interval of time by attaching lease to it.
|
||||
|
||||
Here is the command to set the value of key `foo1` to `bar1` for 10s.
|
||||
|
||||
```bash
|
||||
$ etcdctl put foo1 bar1 --lease=1234abcd
|
||||
OK
|
||||
```
|
||||
|
||||
Note: The lease id `1234abcd` in the above command refers to id returned on creating the lease of 10s. This id can then be attached to the key.
|
||||
|
||||
## Read keys
|
||||
|
||||
Applications can read values of keys from an etcd cluster. Queries may read a single key, or a range of keys.
|
||||
Applications can read values of keys from an etcd cluster. Queries may read a single key, or a range of keys.
|
||||
|
||||
Suppose the etcd cluster has stored the following keys:
|
||||
|
||||
```bash
|
||||
```
|
||||
foo = bar
|
||||
foo1 = bar1
|
||||
foo2 = bar2
|
||||
foo3 = bar3
|
||||
```
|
||||
|
||||
@ -63,59 +39,18 @@ foo
|
||||
bar
|
||||
```
|
||||
|
||||
Here is the command to read the value of key `foo` in hex format:
|
||||
Here is the command to range over the keys from `foo` to `foo9`:
|
||||
|
||||
```bash
|
||||
$ etcdctl get foo --hex
|
||||
\x66\x6f\x6f # Key
|
||||
\x62\x61\x72 # Value
|
||||
```
|
||||
|
||||
Here is the command to read only the value of key `foo`:
|
||||
|
||||
```bash
|
||||
$ etcdctl get foo --print-value-only
|
||||
bar
|
||||
```
|
||||
|
||||
Here is the command to range over the keys from `foo` to `foo3`:
|
||||
|
||||
```bash
|
||||
$ etcdctl get foo foo3
|
||||
$ etcdctl get foo foo9
|
||||
foo
|
||||
bar
|
||||
foo1
|
||||
bar1
|
||||
foo2
|
||||
bar2
|
||||
```
|
||||
|
||||
Note that `foo3` is excluded since the range is over the half-open interval `[foo, foo3)`, excluding `foo3`.
|
||||
|
||||
Here is the command to range over all keys prefixed with `foo`:
|
||||
|
||||
```bash
|
||||
$ etcdctl get --prefix foo
|
||||
foo
|
||||
bar
|
||||
foo1
|
||||
bar1
|
||||
foo2
|
||||
bar2
|
||||
foo3
|
||||
bar3
|
||||
```
|
||||
|
||||
Here is the command to range over all keys prefixed with `foo`, limiting the number of results to 2:
|
||||
|
||||
```bash
|
||||
$ etcdctl get --prefix --limit=2 foo
|
||||
foo
|
||||
bar
|
||||
foo1
|
||||
bar1
|
||||
```
|
||||
|
||||
## Read past version of keys
|
||||
|
||||
Applications may want to read superseded versions of a key. For example, an application may wish to roll back to an old configuration by accessing an earlier version of a key. Alternatively, an application may want a consistent view over multiple keys through multiple requests by accessing key history.
|
||||
@ -123,81 +58,45 @@ Since every modification to the etcd cluster key-value store increments the glob
|
||||
|
||||
Suppose an etcd cluster already has the following keys:
|
||||
|
||||
```bash
|
||||
foo = bar # revision = 2
|
||||
foo1 = bar1 # revision = 3
|
||||
foo = bar_new # revision = 4
|
||||
foo1 = bar1_new # revision = 5
|
||||
``` bash
|
||||
$ etcdctl put foo bar # revision = 2
|
||||
$ etcdctl put foo1 bar1 # revision = 3
|
||||
$ etcdctl put foo bar_new # revision = 4
|
||||
$ etcdctl put foo1 bar1_new # revision = 5
|
||||
```
|
||||
|
||||
Here are an example to access the past versions of keys:
|
||||
|
||||
```bash
|
||||
$ etcdctl get --prefix foo # access the most recent versions of keys
|
||||
$ etcdctl get foo foo9 # access the most recent versions of keys
|
||||
foo
|
||||
bar_new
|
||||
foo1
|
||||
bar1_new
|
||||
|
||||
$ etcdctl get --prefix --rev=4 foo # access the versions of keys at revision 4
|
||||
$ etcdctl get --rev=4 foo foo9 # access the versions of keys at revision 4
|
||||
foo
|
||||
bar_new
|
||||
foo1
|
||||
bar1
|
||||
|
||||
$ etcdctl get --prefix --rev=3 foo # access the versions of keys at revision 3
|
||||
$ etcdctl get --rev=3 foo foo9 # access the versions of keys at revision 3
|
||||
foo
|
||||
bar
|
||||
foo1
|
||||
bar1
|
||||
|
||||
$ etcdctl get --prefix --rev=2 foo # access the versions of keys at revision 2
|
||||
$ etcdctl get --rev=2 foo foo9 # access the versions of keys at revision 2
|
||||
foo
|
||||
bar
|
||||
|
||||
$ etcdctl get --prefix --rev=1 foo # access the versions of keys at revision 1
|
||||
```
|
||||
|
||||
## Read keys which are greater than or equal to the byte value of the specified key
|
||||
|
||||
Applications may want to read keys which are greater than or equal to the byte value of the specified key.
|
||||
|
||||
Suppose an etcd cluster already has the following keys:
|
||||
|
||||
```bash
|
||||
a = 123
|
||||
b = 456
|
||||
z = 789
|
||||
```
|
||||
|
||||
Here is the command to read keys which are greater than or equal to the byte value of key `b` :
|
||||
|
||||
```bash
|
||||
$ etcdctl get --from-key b
|
||||
b
|
||||
456
|
||||
z
|
||||
789
|
||||
$ etcdctl get --rev=1 foo foo9 # access the versions of keys at revision 1
|
||||
```
|
||||
|
||||
## Delete keys
|
||||
|
||||
Applications can delete a key or a range of keys from an etcd cluster.
|
||||
|
||||
Suppose an etcd cluster already has the following keys:
|
||||
|
||||
```bash
|
||||
foo = bar
|
||||
foo1 = bar1
|
||||
foo3 = bar3
|
||||
zoo = val
|
||||
zoo1 = val1
|
||||
zoo2 = val2
|
||||
a = 123
|
||||
b = 456
|
||||
z = 789
|
||||
```
|
||||
|
||||
Here is the command to delete key `foo`:
|
||||
|
||||
```bash
|
||||
@ -212,29 +111,6 @@ $ etcdctl del foo foo9
|
||||
2 # two keys are deleted
|
||||
```
|
||||
|
||||
Here is the command to delete key `zoo` with the deleted key value pair returned:
|
||||
|
||||
```bash
|
||||
$ etcdctl del --prev-kv zoo
|
||||
1 # one key is deleted
|
||||
zoo # deleted key
|
||||
val # the value of the deleted key
|
||||
```
|
||||
|
||||
Here is the command to delete keys having prefix as `zoo`:
|
||||
|
||||
```bash
|
||||
$ etcdctl del --prefix zoo
|
||||
2 # two keys are deleted
|
||||
```
|
||||
|
||||
Here is the command to delete keys which are greater than or equal to the byte value of key `b` :
|
||||
|
||||
```bash
|
||||
$ etcdctl del --from-key b
|
||||
2 # two keys are deleted
|
||||
```
|
||||
|
||||
## Watch key changes
|
||||
|
||||
Applications can watch on a key or a range of keys to monitor for any updates.
|
||||
@ -242,86 +118,38 @@ Applications can watch on a key or a range of keys to monitor for any updates.
|
||||
Here is the command to watch on key `foo`:
|
||||
|
||||
```bash
|
||||
$ etcdctl watch foo
|
||||
$ etcdctl watch foo
|
||||
# in another terminal: etcdctl put foo bar
|
||||
PUT
|
||||
foo
|
||||
bar
|
||||
```
|
||||
|
||||
Here is the command to watch on key `foo` in hex format:
|
||||
|
||||
```bash
|
||||
$ etcdctl watch foo --hex
|
||||
# in another terminal: etcdctl put foo bar
|
||||
PUT
|
||||
\x66\x6f\x6f # Key
|
||||
\x62\x61\x72 # Value
|
||||
```
|
||||
|
||||
Here is the command to watch on a range key from `foo` to `foo9`:
|
||||
|
||||
```bash
|
||||
$ etcdctl watch foo foo9
|
||||
# in another terminal: etcdctl put foo bar
|
||||
PUT
|
||||
foo
|
||||
bar
|
||||
# in another terminal: etcdctl put foo1 bar1
|
||||
PUT
|
||||
foo1
|
||||
bar1
|
||||
```
|
||||
|
||||
Here is the command to watch on keys having prefix `foo`:
|
||||
|
||||
```bash
|
||||
$ etcdctl watch --prefix foo
|
||||
# in another terminal: etcdctl put foo bar
|
||||
PUT
|
||||
foo
|
||||
bar
|
||||
# in another terminal: etcdctl put fooz1 barz1
|
||||
PUT
|
||||
fooz1
|
||||
barz1
|
||||
```
|
||||
|
||||
Here is the command to watch on multiple keys `foo` and `zoo`:
|
||||
|
||||
```bash
|
||||
$ etcdctl watch -i
|
||||
$ watch foo
|
||||
$ watch zoo
|
||||
# in another terminal: etcdctl put foo bar
|
||||
PUT
|
||||
foo
|
||||
bar
|
||||
# in another terminal: etcdctl put zoo val
|
||||
PUT
|
||||
zoo
|
||||
val
|
||||
```
|
||||
|
||||
## Watch historical changes of keys
|
||||
|
||||
Applications may want to watch for historical changes of keys in etcd. For example, an application may wish to receive all the modifications of a key; if the application stays connected to etcd, then `watch` is good enough. However, if the application or etcd fails, a change may happen during the failure, and the application will not receive the update in real time. To guarantee the update is delivered, the application must be able to watch for historical changes to keys. To do this, an application can specify a historical revision on a watch, just like reading past version of keys.
|
||||
|
||||
Suppose we finished the following sequence of operations:
|
||||
|
||||
```bash
|
||||
$ etcdctl put foo bar # revision = 2
|
||||
OK
|
||||
$ etcdctl put foo1 bar1 # revision = 3
|
||||
OK
|
||||
$ etcdctl put foo bar_new # revision = 4
|
||||
OK
|
||||
$ etcdctl put foo1 bar1_new # revision = 5
|
||||
OK
|
||||
``` bash
|
||||
etcdctl put foo bar # revision = 2
|
||||
etcdctl put foo1 bar1 # revision = 3
|
||||
etcdctl put foo bar_new # revision = 4
|
||||
etcdctl put foo1 bar1_new # revision = 5
|
||||
```
|
||||
|
||||
Here is an example to watch the historical changes:
|
||||
|
||||
```bash
|
||||
# watch for changes on key `foo` since revision 2
|
||||
$ etcdctl watch --rev=2 foo
|
||||
@ -331,9 +159,7 @@ bar
|
||||
PUT
|
||||
foo
|
||||
bar_new
|
||||
```
|
||||
|
||||
```bash
|
||||
# watch for changes on key `foo` since revision 3
|
||||
$ etcdctl watch --rev=3 foo
|
||||
PUT
|
||||
@ -341,19 +167,6 @@ foo
|
||||
bar_new
|
||||
```
|
||||
|
||||
Here is an example to watch only from the last historical change:
|
||||
|
||||
```bash
|
||||
# watch for changes on key `foo` and return last revision value along with modified value
|
||||
$ etcdctl watch --prev-kv foo
|
||||
# in another terminal: etcdctl put foo bar_latest
|
||||
PUT
|
||||
foo # key
|
||||
bar_new # last value of foo key before modification
|
||||
foo # key
|
||||
bar_latest # value of foo key after modification
|
||||
```
|
||||
|
||||
## Compacted revisions
|
||||
|
||||
As we mentioned, etcd keeps revisions so that applications can read past versions of keys. However, to avoid accumulating an unbounded amount of history, it is important to compact past revisions. After compacting, etcd removes historical revisions, releasing resources for future use. All superseded data with revisions before the compacted revision will be unavailable.
|
||||
@ -369,20 +182,13 @@ $ etcdctl get --rev=4 foo
|
||||
Error: rpc error: code = 11 desc = etcdserver: mvcc: required revision has been compacted
|
||||
```
|
||||
|
||||
Note: The current revision of etcd server can be found using get command on any key (existent or non-existent) in json format. Example is shown below for mykey which does not exist in etcd server:
|
||||
|
||||
```bash
|
||||
$ etcdctl get mykey -w=json
|
||||
{"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":15,"raft_term":4}}
|
||||
```
|
||||
|
||||
## Grant leases
|
||||
|
||||
Applications can grant leases for keys from an etcd cluster. When a key is attached to a lease, its lifetime is bound to the lease's lifetime which in turn is governed by a time-to-live (TTL). Each lease has a minimum time-to-live (TTL) value specified by the application at grant time. The lease's actual TTL value is at least the minimum TTL and is chosen by the etcd cluster. Once a lease's TTL elapses, the lease expires and all attached keys are deleted.
|
||||
|
||||
Here is the command to grant a lease:
|
||||
|
||||
```bash
|
||||
```
|
||||
# grant a lease with 10 second TTL
|
||||
$ etcdctl lease grant 10
|
||||
lease 32695410dcc0ca06 granted with TTL(10s)
|
||||
@ -398,7 +204,7 @@ Applications revoke leases by lease ID. Revoking a lease deletes all of its atta
|
||||
|
||||
Suppose we finished the following sequence of operations:
|
||||
|
||||
```bash
|
||||
```
|
||||
$ etcdctl lease grant 10
|
||||
lease 32695410dcc0ca06 granted with TTL(10s)
|
||||
$ etcdctl put --lease=32695410dcc0ca06 foo bar
|
||||
@ -407,7 +213,7 @@ OK
|
||||
|
||||
Here is the command to revoke the same lease:
|
||||
|
||||
```bash
|
||||
```
|
||||
$ etcdctl lease revoke 32695410dcc0ca06
|
||||
lease 32695410dcc0ca06 revoked
|
||||
|
||||
@ -421,55 +227,17 @@ Applications can keep a lease alive by refreshing its TTL so it does not expire.
|
||||
|
||||
Suppose we finished the following sequence of operations:
|
||||
|
||||
```bash
|
||||
```
|
||||
$ etcdctl lease grant 10
|
||||
lease 32695410dcc0ca06 granted with TTL(10s)
|
||||
```
|
||||
|
||||
Here is the command to keep the same lease alive:
|
||||
|
||||
```bash
|
||||
$ etcdctl lease keep-alive 32695410dcc0ca06
|
||||
lease 32695410dcc0ca06 keepalived with TTL(100)
|
||||
lease 32695410dcc0ca06 keepalived with TTL(100)
|
||||
lease 32695410dcc0ca06 keepalived with TTL(100)
|
||||
```
|
||||
$ etcdctl lease keep-alive 32695410dcc0ca0
|
||||
lease 32695410dcc0ca0 keepalived with TTL(100)
|
||||
lease 32695410dcc0ca0 keepalived with TTL(100)
|
||||
lease 32695410dcc0ca0 keepalived with TTL(100)
|
||||
...
|
||||
```
|
||||
|
||||
## Get lease information
|
||||
|
||||
Applications may want to know about lease information, so that they can be renewed or to check if the lease still exists or it has expired. Applications may also want to know the keys to which a particular lease is attached.
|
||||
|
||||
Suppose we finished the following sequence of operations:
|
||||
|
||||
```bash
|
||||
# grant a lease with 500 second TTL
|
||||
$ etcdctl lease grant 500
|
||||
lease 694d5765fc71500b granted with TTL(500s)
|
||||
|
||||
# attach key zoo1 to lease 694d5765fc71500b
|
||||
$ etcdctl put zoo1 val1 --lease=694d5765fc71500b
|
||||
OK
|
||||
|
||||
# attach key zoo2 to lease 694d5765fc71500b
|
||||
$ etcdctl put zoo2 val2 --lease=694d5765fc71500b
|
||||
OK
|
||||
```
|
||||
|
||||
Here is the command to get information about the lease:
|
||||
|
||||
```bash
|
||||
$ etcdctl lease timetolive 694d5765fc71500b
|
||||
lease 694d5765fc71500b granted with TTL(500s), remaining(258s)
|
||||
```
|
||||
|
||||
Here is the command to get information about the lease along with the keys attached with the lease:
|
||||
|
||||
```bash
|
||||
$ etcdctl lease timetolive --keys 694d5765fc71500b
|
||||
lease 694d5765fc71500b granted with TTL(500s), remaining(132s), attached keys([zoo2 zoo1])
|
||||
|
||||
# if the lease has expired or does not exist it will give the below response:
|
||||
Error: etcdserver: requested lease not found
|
||||
```
|
||||
|
||||
|
@ -1,10 +0,0 @@
|
||||
# System limits
|
||||
|
||||
## Request size limit
|
||||
|
||||
etcd is designed to handle small key value pairs typical for metadata. Larger requests will work, but may increase the latency of other requests. For the time being, etcd guarantees to support RPC requests with up to 1MB of data. In the future, the size limit may be loosened or made it configurable.
|
||||
|
||||
## Storage size limit
|
||||
|
||||
The default storage size limit is 2GB, configurable with `--quota-backend-bytes` flag; supports up to 8GB.
|
||||
|
@ -28,7 +28,7 @@ bar
|
||||
|
||||
## Local multi-member cluster
|
||||
|
||||
A `Procfile` at the base of this git repo is provided to easily set up a local multi-member cluster. To start a multi-member cluster go to the root of an etcd source tree and run:
|
||||
A Procfile is provided to easily set up a local multi-member cluster. Start a multi-member cluster with a few commands:
|
||||
|
||||
```
|
||||
# install goreman program to control Profile-based applications.
|
||||
@ -37,7 +37,7 @@ $ goreman -f Procfile start
|
||||
...
|
||||
```
|
||||
|
||||
The started members listen on `localhost:2379`, `localhost:22379`, and `localhost:32379` for client requests respectively.
|
||||
The started members listen on `localhost:12379`, `localhost:22379`, and `localhost:32379` for client requests respectively.
|
||||
|
||||
To interact with the started cluster by using etcdctl:
|
||||
|
||||
@ -45,16 +45,16 @@ To interact with the started cluster by using etcdctl:
|
||||
# use API version 3
|
||||
$ export ETCDCTL_API=3
|
||||
|
||||
$ etcdctl --write-out=table --endpoints=localhost:2379 member list
|
||||
$ etcdctl --write-out=table --endpoints=localhost:12379 member list
|
||||
+------------------+---------+--------+------------------------+------------------------+
|
||||
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS |
|
||||
+------------------+---------+--------+------------------------+------------------------+
|
||||
| 8211f1d0f64f3269 | started | infra1 | http://127.0.0.1:2380 | http://127.0.0.1:2379 |
|
||||
| 8211f1d0f64f3269 | started | infra1 | http://127.0.0.1:12380 | http://127.0.0.1:12379 |
|
||||
| 91bc3c398fb3c146 | started | infra2 | http://127.0.0.1:22380 | http://127.0.0.1:22379 |
|
||||
| fd422379fda50e48 | started | infra3 | http://127.0.0.1:32380 | http://127.0.0.1:32379 |
|
||||
+------------------+---------+--------+------------------------+------------------------+
|
||||
|
||||
$ etcdctl put foo bar
|
||||
$ etcdctl --endpoints=localhost:12379 put foo bar
|
||||
OK
|
||||
```
|
||||
|
||||
@ -64,10 +64,10 @@ To exercise etcd's fault tolerance, kill a member:
|
||||
# kill etcd2
|
||||
$ goreman run stop etcd2
|
||||
|
||||
$ etcdctl put key hello
|
||||
$ etcdctl --endpoints=localhost:12379 put key hello
|
||||
OK
|
||||
|
||||
$ etcdctl get key
|
||||
$ etcdctl --endpoints=localhost:12379 get key
|
||||
hello
|
||||
|
||||
# try to get key from the killed member
|
||||
|
@ -31,8 +31,8 @@ All releases version numbers follow the format of [semantic versioning 2.0.0](ht
|
||||
## Write release note
|
||||
|
||||
- Write introduction for the new release. For example, what major bug we fix, what new features we introduce or what performance improvement we make.
|
||||
- Write changelog for the last release. ChangeLog should be straightforward and easy to understand for the end-user.
|
||||
- Put `[GH XXXX]` at the head of change line to reference Pull Request that introduces the change. Moreover, add a link on it to jump to the Pull Request.
|
||||
- Find PRs with `release-note` label and explain them in `NEWS` file, as a straightforward summary of changes for end-users.
|
||||
|
||||
## Tag version
|
||||
|
||||
@ -47,7 +47,7 @@ All releases version numbers follow the format of [semantic versioning 2.0.0](ht
|
||||
|
||||
## Build release binaries and images
|
||||
|
||||
- Ensure `acbuild` is available.
|
||||
- Ensure `actool` is available, or installing it through `go get github.com/appc/spec/actool`.
|
||||
- Ensure `docker` is available.
|
||||
|
||||
Run release script in root directory:
|
||||
|
@ -10,44 +10,27 @@ The easiest way to get etcd is to use one of the pre-built release binaries whic
|
||||
|
||||
## Build the latest version
|
||||
|
||||
For those wanting to try the very latest version, build etcd from the `master` branch. [Go](https://golang.org/) version 1.7+ is required to build the latest version of etcd. To ensure etcd is built against well-tested libraries, etcd vendors its dependencies for official release binaries. However, etcd's vendoring is also optional to avoid potential import conflicts when embedding the etcd server or using the etcd client.
|
||||
For those wanting to try the very latest version, build etcd from the `master` branch.
|
||||
[Go](https://golang.org/) version 1.6+ (with HTTP2 support) is required to build the latest version of etcd.
|
||||
|
||||
First, confirm go 1.7+ is installed:
|
||||
|
||||
```sh
|
||||
# go is required
|
||||
$ go version
|
||||
go version go1.7.3 darwin/amd64
|
||||
Here are the commands to build an etcd binary from the `master` branch:
|
||||
|
||||
```
|
||||
# go is required
|
||||
$ go version
|
||||
go version go1.6 darwin/amd64
|
||||
|
||||
To build `etcd` from the `master` branch without a `GOPATH` using the official `build` script:
|
||||
# GOPATH should be set correctly
|
||||
$ echo $GOPATH
|
||||
/Users/example/go
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/coreos/etcd.git
|
||||
$ mkdir -p $GOPATH/src/github.com/coreos
|
||||
$ cd $GOPATH/src/github.com/coreos
|
||||
$ git clone github.com:coreos/etcd.git
|
||||
$ cd etcd
|
||||
$ ./build
|
||||
$ ./bin/etcd
|
||||
```
|
||||
|
||||
To build a vendored `etcd` from the `master` branch via `go get`:
|
||||
|
||||
```sh
|
||||
# GOPATH should be set
|
||||
$ echo $GOPATH
|
||||
/Users/example/go
|
||||
$ go get github.com/coreos/etcd/cmd/etcd
|
||||
$ $GOPATH/bin/etcd
|
||||
```
|
||||
|
||||
To build `etcd` from the `master` branch without vendoring (may not build due to upstream conflicts):
|
||||
|
||||
```sh
|
||||
# GOPATH should be set
|
||||
$ echo $GOPATH
|
||||
/Users/example/go
|
||||
$ go get github.com/coreos/etcd
|
||||
$ $GOPATH/bin/etcd
|
||||
...
|
||||
```
|
||||
|
||||
## Test the installation
|
||||
@ -71,6 +54,3 @@ If OK is printed, then etcd is working!
|
||||
|
||||
[github-release]: https://github.com/coreos/etcd/releases/
|
||||
[go]: https://golang.org/doc/install
|
||||
[build-script]: ../build
|
||||
[cmd-directory]: ../cmd
|
||||
|
||||
|
@ -14,23 +14,17 @@ The easiest way to get started using etcd as a distributed key-value store is to
|
||||
- [Interacting with etcd][interacting]
|
||||
- [API references][api_ref]
|
||||
- [gRPC gateway][api_grpc_gateway]
|
||||
- [gRPC naming and discovery][grpc_naming]
|
||||
- [Embedding etcd][embed_etcd]
|
||||
- [Experimental features and APIs][experimental]
|
||||
- [System limits][system-limit]
|
||||
|
||||
## Operating etcd clusters
|
||||
|
||||
Administrators who need to create reliable and scalable key-value stores for the developers they support should begin with a [cluster on multiple machines][clustering].
|
||||
|
||||
- [Setting up etcd clusters][clustering]
|
||||
- [Setting up etcd gateways][gateway]
|
||||
- [Setting up etcd gRPC proxy (pre-alpha)][grpc_proxy]
|
||||
- [Setting up clusters][clustering]
|
||||
- [Run etcd clusters inside containers][container]
|
||||
- [Hardware recommendations][hardware]
|
||||
- [Configuration][conf]
|
||||
- [Security][security]
|
||||
- [Monitoring][monitoring]
|
||||
- Monitoring
|
||||
- [Maintenance][maintenance]
|
||||
- [Understand failures][failures]
|
||||
- [Disaster recovery][recovery]
|
||||
@ -42,7 +36,7 @@ Administrators who need to create reliable and scalable key-value stores for the
|
||||
|
||||
To learn more about the concepts and internals behind etcd, read the following pages:
|
||||
|
||||
- [Why etcd][why] (TODO)
|
||||
- Why etcd (TODO)
|
||||
- [Understand data model][data_model]
|
||||
- [Understand APIs][understand_apis]
|
||||
- [Glossary][glossary]
|
||||
@ -52,36 +46,24 @@ To learn more about the concepts and internals behind etcd, read the following p
|
||||
|
||||
- [Migrate applications from using API v2 to API v3][v2_migration]
|
||||
- [Updating v2.3 to v3.0][v3_upgrade]
|
||||
- [Updating v3.0 to v3.1][v31_upgrade]
|
||||
|
||||
## Frequently Asked Questions (FAQ)
|
||||
|
||||
Answers to [common questions] about etcd.
|
||||
## Troubleshooting
|
||||
|
||||
[api_ref]: dev-guide/api_reference_v3.md
|
||||
[api_grpc_gateway]: dev-guide/api_grpc_gateway.md
|
||||
[clustering]: op-guide/clustering.md
|
||||
[conf]: op-guide/configuration.md
|
||||
[system-limit]: dev-guide/limit.md
|
||||
[common questions]: faq.md
|
||||
[why]: learning/why.md
|
||||
[data_model]: learning/data_model.md
|
||||
[demo]: demo.md
|
||||
[download_build]: dl_build.md
|
||||
[embed_etcd]: https://godoc.org/github.com/coreos/etcd/embed
|
||||
[grpc_naming]: dev-guide/grpc_naming.md
|
||||
[failures]: op-guide/failures.md
|
||||
[gateway]: op-guide/gateway.md
|
||||
[glossary]: learning/glossary.md
|
||||
[grpc_proxy]: op-guide/grpc_proxy.md
|
||||
[hardware]: op-guide/hardware.md
|
||||
[interacting]: dev-guide/interacting_v3.md
|
||||
[local_cluster]: dev-guide/local_cluster.md
|
||||
[performance]: op-guide/performance.md
|
||||
[recovery]: op-guide/recovery.md
|
||||
[maintenance]: op-guide/maintenance.md
|
||||
[security]: op-guide/security.md
|
||||
[monitoring]: op-guide/monitoring.md
|
||||
[v2_migration]: op-guide/v2-migration.md
|
||||
[container]: op-guide/container.md
|
||||
[understand_apis]: learning/api.md
|
||||
@ -89,4 +71,3 @@ Answers to [common questions] about etcd.
|
||||
[supported_platform]: op-guide/supported-platform.md
|
||||
[experimental]: dev-guide/experimental_apis.md
|
||||
[v3_upgrade]: upgrades/upgrade_3_0.md
|
||||
[v31_upgrade]: upgrades/upgrade_3_1.md
|
||||
|
@ -1,128 +0,0 @@
|
||||
## Frequently Asked Questions (FAQ)
|
||||
|
||||
### etcd, general
|
||||
|
||||
#### Do clients have to send requests to the etcd leader?
|
||||
|
||||
[Raft][raft] is leader-based; the leader handles all client requests which need cluster consensus. However, the client does not need to know which node is the leader. Any request that requires consensus sent to a follower is automatically forwarded to the leader. Requests that do not require consensus (e.g., serialized reads) can be processed by any cluster member.
|
||||
|
||||
### Configuration
|
||||
|
||||
#### What is the difference between advertise-urls and listen-urls?
|
||||
|
||||
`listen-urls` specifies the local addresses etcd server binds to for accepting incoming connections. To listen on a port for all interfaces, specify `0.0.0.0` as the listen IP address.
|
||||
|
||||
`advertise-urls` specifies the addresses etcd clients or other etcd members should use to contact the etcd server. The advertise addresses must be reachable from the remote machines. Do not advertise addresses like `localhost` or `0.0.0.0` for a production setup since these addresses are unreachable from remote machines.
|
||||
|
||||
### Deployment
|
||||
|
||||
#### System requirements
|
||||
|
||||
Since etcd writes data to disk, SSD is highly recommended. To prevent performance degradation or unintentionally overloading the key-value store, etcd enforces a 2GB default storage size quota, configurable up to 8GB. To avoid swapping or running out of memory, the machine should have at least as much RAM to cover the quota. At CoreOS, an etcd cluster is usually deployed on dedicated CoreOS Container Linux machines with dual-core processors, 2GB of RAM, and 80GB of SSD *at the very least*. **Note that performance is intrinsically workload dependent; please test before production deployment**. See [hardware][hardware-setup] for more recommendations.
|
||||
|
||||
Most stable production environment is Linux operating system with amd64 architecture; see [supported platform][supported-platform] for more.
|
||||
|
||||
#### Why an odd number of cluster members?
|
||||
|
||||
An etcd cluster needs a majority of nodes, a quorum, to agree on updates to the cluster state. For a cluster with n members, quorum is (n/2)+1. For any odd-sized cluster, adding one node will always increase the number of nodes necessary for quorum. Although adding a node to an odd-sized cluster appears better since there are more machines, the fault tolerance is worse since exactly the same number of nodes may fail without losing quorum but there are more nodes that can fail. If the cluster is in a state where it can't tolerate any more failures, adding a node before removing nodes is dangerous because if the new node fails to register with the cluster (e.g., the address is misconfigured), quorum will be permanently lost.
|
||||
|
||||
#### What is maximum cluster size?
|
||||
|
||||
Theoretically, there is no hard limit. However, an etcd cluster probably should have no more than seven nodes. [Google Chubby lock service][chubby], similar to etcd and widely deployed within Google for many years, suggests running five nodes. A 5-member etcd cluster can tolerate two member failures, which is enough in most cases. Although larger clusters provide better fault tolerance, the write performance suffers because data must be replicated across more machines.
|
||||
|
||||
#### What is failure tolerance?
|
||||
|
||||
An etcd cluster operates so long as a member quorum can be established. If quorum is lost through transient network failures (e.g., partitions), etcd automatically and safely resumes once the network recovers and restores quorum; Raft enforces cluster consistency. For power loss, etcd persists the Raft log to disk; etcd replays the log to the point of failure and resumes cluster participation. For permanent hardware failure, the node may be removed from the cluster through [runtime reconfiguration][runtime reconfiguration].
|
||||
|
||||
It is recommended to have an odd number of members in a cluster. An odd-size cluster tolerates the same number of failures as an even-size cluster but with fewer nodes. The difference can be seen by comparing even and odd sized clusters:
|
||||
|
||||
| Cluster Size | Majority | Failure Tolerance |
|
||||
|:-:|:-:|:-:|
|
||||
| 1 | 1 | 0 |
|
||||
| 2 | 2 | 0 |
|
||||
| 3 | 2 | 1 |
|
||||
| 4 | 3 | 1 |
|
||||
| 5 | 3 | 2 |
|
||||
| 6 | 4 | 2 |
|
||||
| 7 | 4 | 3 |
|
||||
| 8 | 5 | 3 |
|
||||
| 9 | 5 | 4 |
|
||||
|
||||
Adding a member to bring the size of cluster up to an even number doesn't buy additional fault tolerance. Likewise, during a network partition, an odd number of members guarantees that there will always be a majority partition that can continue to operate and be the source of truth when the partition ends.
|
||||
|
||||
#### Does etcd work in cross-region or cross data center deployments?
|
||||
|
||||
Deploying etcd across regions improves etcd's fault tolerance since members are in separate failure domains. The cost is higher consensus request latency from crossing data center boundaries. Since etcd relies on a member quorum for consensus, the latency from crossing data centers will be somewhat pronounced because at least a majority of cluster members must respond to consensus requests. Additionally, cluster data must be replicated across all peers, so there will be bandwidth cost as well.
|
||||
|
||||
With longer latencies, the default etcd configuration may cause frequent elections or heartbeat timeouts. See [tuning] for adjusting timeouts for high latency deployments.
|
||||
|
||||
### Operation
|
||||
|
||||
#### How to backup a etcd cluster?
|
||||
|
||||
etcdctl provides a `snapshot` command to create backups. See [backup][backup] for more details.
|
||||
|
||||
#### Should I add a member before removing an unhealthy member?
|
||||
|
||||
When replacing an etcd node, it's important to remove the member first and then add its replacement.
|
||||
|
||||
etcd employs distributed consensus based on a quorum model; (n+1)/2 members, a majority, must agree on a proposal before it can be committed to the cluster. These proposals include key-value updates and membership changes. This model totally avoids any possibility of split brain inconsistency. The downside is permanent quorum loss is catastrophic.
|
||||
|
||||
How this applies to membership: If a 3-member cluster has 1 downed member, it can still make forward progress because the quorum is 2 and 2 members are still live. However, adding a new member to a 3-member cluster will increase the quorum to 3 because 3 votes are required for a majority of 4 members. Since the quorum increased, this extra member buys nothing in terms of fault tolerance; the cluster is still one node failure away from being unrecoverable.
|
||||
|
||||
Additionally, that new member is risky because it may turn out to be misconfigured or incapable of joining the cluster. In that case, there's no way to recover quorum because the cluster has two members down and two members up, but needs three votes to change membership to undo the botched membership addition. etcd will by default reject member add attempts that could take down the cluster in this manner.
|
||||
|
||||
On the other hand, if the downed member is removed from cluster membership first, the number of members becomes 2 and the quorum remains at 2. Following that removal by adding a new member will also keep the quorum steady at 2. So, even if the new node can't be brought up, it's still possible to remove the new member through quorum on the remaining live members.
|
||||
|
||||
#### Why won't etcd accept my membership changes?
|
||||
|
||||
etcd sets `strict-reconfig-check` in order to reject reconfiguration requests that would cause quorum loss. Abandoning quorum is really risky (especially when the cluster is already unhealthy). Although it may be tempting to disable quorum checking if there's quorum loss to add a new member, this could lead to full fledged cluster inconsistency. For many applications, this will make the problem even worse ("disk geometry corruption" being a candidate for most terrifying).
|
||||
|
||||
### Performance
|
||||
|
||||
#### How should I benchmark etcd?
|
||||
|
||||
Try the [benchmark] tool. Current [benchmark results][benchmark-result] are available for comparison.
|
||||
|
||||
#### What does the etcd warning "apply entries took too long" mean?
|
||||
|
||||
After a majority of etcd members agree to commit a request, each etcd server applies the request to its data store and persists the result to disk. Even with a slow mechanical disk or a virtualized network disk, such as Amazon’s EBS or Google’s PD, applying a request should normally take fewer than 50 milliseconds. If the average apply duration exceeds 100 milliseconds, etcd will warn that entries are taking too long to apply.
|
||||
|
||||
Usually this issue is caused by a slow disk. The disk could be experiencing contention among etcd and other applications, or the disk is too simply slow (e.g., a shared virtualized disk). To rule out a slow disk from causing this warning, monitor [backend_commit_duration_seconds][backend_commit_metrics] (p99 duration should be less than 25ms) to confirm the disk is reasonably fast. If the disk is too slow, assigning a dedicated disk to etcd or using faster disk will typically solve the problem.
|
||||
|
||||
The second most common cause is CPU starvation. If monitoring of the machine’s CPU usage shows heavy utilization, there may not be enough compute capacity for etcd. Moving etcd to dedicated machine, increasing process resource isolation cgroups, or renicing the etcd server process into a higher priority can usually solve the problem.
|
||||
|
||||
Expensive user requests which access too many keys (e.g., fetching the entire keyspace) can also cause long apply latencies. Accessing fewer than a several hundred keys per request, however, should always be performant.
|
||||
|
||||
If none of the above suggestions clear the warnings, please [open an issue][new_issue] with detailed logging, monitoring, metrics and optionally workload information.
|
||||
|
||||
#### What does the etcd warning "failed to send out heartbeat on time" mean?
|
||||
|
||||
etcd uses a leader-based consensus protocol for consistent data replication and log execution. Cluster members elect a single leader, all other members become followers. The elected leader must periodically send heartbeats to its followers to maintain its leadership. Followers infer leader failure if no heartbeats are received within an election interval and trigger an election. If a leader doesn’t send its heartbeats in time but is still running, the election is spurious and likely caused by insufficient resources. To catch these soft failures, if the leader skips two heartbeat intervals, etcd will warn it failed to send a heartbeat on time.
|
||||
|
||||
Usually this issue is caused by a slow disk. Before the leader sends heartbeats attached with metadata, it may need to persist the metadata to disk. The disk could be experiencing contention among etcd and other applications, or the disk is too simply slow (e.g., a shared virtualized disk). To rule out a slow disk from causing this warning, monitor [wal_fsync_duration_seconds][wal_fsync_duration_seconds] (p99 duration should be less than 10ms) to confirm the disk is reasonably fast. If the disk is too slow, assigning a dedicated disk to etcd or using faster disk will typically solve the problem.
|
||||
|
||||
The second most common cause is CPU starvation. If monitoring of the machine’s CPU usage shows heavy utilization, there may not be enough compute capacity for etcd. Moving etcd to dedicated machine, increasing process resource isolation with cgroups, or renicing the etcd server process into a higher priority can usually solve the problem.
|
||||
|
||||
A slow network can also cause this issue. If network metrics among the etcd machines shows long latencies or high drop rate, there may not be enough network capacity for etcd. Moving etcd members to a less congested network will typically solve the problem. However, if the etcd cluster is deployed across data centers, long latency between members is expected. For such deployments, tune the `heartbeat-interval` configuration to roughly match the round trip time between the machines, and the `election-timeout` configuration to be at least 5 * `heartbeat-interval`. See [tuning documentation][tuning] for detailed information.
|
||||
|
||||
If none of the above suggestions clear the warnings, please [open an issue][new_issue] with detailed logging, monitoring, metrics and optionally workload information.
|
||||
|
||||
#### What does the etcd warning "request ignored (cluster ID mismatch)" mean?
|
||||
|
||||
Every new etcd cluster generates a new cluster ID based on the initial cluster configuration and a user-provided unique `initial-cluster-token` value. By having unique cluster ID's, etcd is protected from cross-cluster interaction which could corrupt the cluster.
|
||||
|
||||
Usually this warning happens after tearing down an old cluster, then reusing some of the peer addresses for the new cluster. If any etcd process from the old cluster is still running it will try to contact the new cluster. The new cluster will recognize a cluster ID mismatch, then ignore the request and emit this warning. This warning is often cleared by ensuring peer addresses among distinct clusters are disjoint.
|
||||
|
||||
[hardware-setup]: ./op-guide/hardware.md
|
||||
[supported-platform]: ./op-guide/supported-platform.md
|
||||
[wal_fsync_duration_seconds]: ./metrics.md#disk
|
||||
[tuning]: ./tuning.md
|
||||
[new_issue]: https://github.com/coreos/etcd/issues/new
|
||||
[backend_commit_metrics]: ./metrics.md#disk
|
||||
[raft]: https://raft.github.io/raft.pdf
|
||||
[backup]: https://github.com/coreos/etcd/blob/master/Documentation/op-guide/recovery.md#snapshotting-the-keyspace
|
||||
[chubby]: http://static.googleusercontent.com/media/research.google.com/en//archive/chubby-osdi06.pdf
|
||||
[runtime reconfiguration]: https://github.com/coreos/etcd/blob/master/Documentation/op-guide/runtime-configuration.md
|
||||
[benchmark]: https://github.com/coreos/etcd/tree/master/tools/benchmark
|
||||
[benchmark-result]: https://github.com/coreos/etcd/blob/master/Documentation/op-guide/performance.md
|
@ -2,17 +2,15 @@
|
||||
|
||||
This document defines the various terms used in etcd documentation, command line and source code.
|
||||
|
||||
## Alarm
|
||||
## Node
|
||||
|
||||
The etcd server raises an alarm whenever the cluster needs operator intervention to remain reliable.
|
||||
Node is an instance of raft state machine.
|
||||
|
||||
## Authentication
|
||||
It has a unique identification, and records other nodes' progress internally when it is the leader.
|
||||
|
||||
Authentication manages user access permissions for etcd resources.
|
||||
## Member
|
||||
|
||||
## Client
|
||||
|
||||
A client connects to the etcd cluster to issue service requests such as fetching key-value pairs, writing data, or watching for updates.
|
||||
Member is an instance of etcd. It hosts a node, and provides service to clients.
|
||||
|
||||
## Cluster
|
||||
|
||||
@ -20,42 +18,6 @@ Cluster consists of several members.
|
||||
|
||||
The node in each member follows raft consensus protocol to replicate logs. Cluster receives proposals from members, commits them and apply to local store.
|
||||
|
||||
## Compaction
|
||||
|
||||
Compaction discards all etcd event history and superseded keys prior to a given revision. It is used to reclaim storage space in the etcd backend database.
|
||||
|
||||
## Election
|
||||
|
||||
The etcd cluster holds elections among its members to choose a leader as part of the raft consensus protocol.
|
||||
|
||||
## Endpoint
|
||||
|
||||
A URL pointing to an etcd service or resource.
|
||||
|
||||
## Key
|
||||
|
||||
A user-defined identifier for storing and retrieving user-defined values in etcd.
|
||||
|
||||
## Key range
|
||||
|
||||
A set of keys containing either an individual key, a lexical interval for all x such that a < x <= b, or all keys greater than a given key.
|
||||
|
||||
## Keyspace
|
||||
|
||||
The set of all keys in an etcd cluster.
|
||||
|
||||
## Lease
|
||||
|
||||
A short-lived renewable contract that deletes keys associated with it on its expiry.
|
||||
|
||||
## Member
|
||||
|
||||
A logical etcd server that participates in serving an etcd cluster.
|
||||
|
||||
## Modification Revision
|
||||
|
||||
The first revision to hold the last write to a given key.
|
||||
|
||||
## Peer
|
||||
|
||||
Peer is another member of the same cluster.
|
||||
@ -64,34 +26,10 @@ Peer is another member of the same cluster.
|
||||
|
||||
A proposal is a request (for example a write request, a configuration change request) that needs to go through raft protocol.
|
||||
|
||||
## Quorum
|
||||
## Client
|
||||
|
||||
The number of active members needed for consensus to modify the cluster state. etcd requires a member majority to reach quorum.
|
||||
Client is a caller of the cluster's HTTP API.
|
||||
|
||||
## Revision
|
||||
## Machine (deprecated)
|
||||
|
||||
A 64-bit cluster-wide counter that is incremented each time the keyspace is modified.
|
||||
|
||||
## Role
|
||||
|
||||
A unit of permissions over a set of key ranges which may be granted to a set of users for access control.
|
||||
|
||||
## Snapshot
|
||||
|
||||
A point-in-time backup of the etcd cluster state.
|
||||
|
||||
## Store
|
||||
|
||||
The physical storage backing the cluster keyspace.
|
||||
|
||||
## Transaction
|
||||
|
||||
An atomically executed set of operations. All modified keys in a transaction share the same modification revision.
|
||||
|
||||
## Key Version
|
||||
|
||||
The number of writes to a key since it was created, starting at 1. The version of a nonexistent or deleted key is 0.
|
||||
|
||||
## Watcher
|
||||
|
||||
A client opens a watcher to observe updates on a given key range.
|
||||
The alternative of Member in etcd before 2.0
|
||||
|
@ -1,21 +0,0 @@
|
||||
# Why etcd
|
||||
|
||||
The name "etcd" originated from two ideas, the unix "/etc" folder and "d"istibuted systems. The "/etc" folder is a place to store configuration data for a single system whereas etcd stores configuration information for large scale distributed systems. Hence, a "d"istributed "/etc" is "etcd".
|
||||
|
||||
etcd stores metadata in a consistent and fault-tolerant way. Distributed systems use etcd as a consistent key-value store for configuration management, service discovery, and coordinating distributed work. Common distributed patterns using etcd include leader election, [distributed locks][etcd-concurrency], and monitoring machine liveness.
|
||||
|
||||
## Use cases
|
||||
|
||||
- Container Linux by CoreOS: Application running on [Container Linux][container-linux] gets automatic, zero-downtime Linux kernel updates. Container Linux uses [locksmith] to coordinate updates. locksmith implements a distributed semaphore over etcd to ensure only a subset of a cluster is rebooting at any given time.
|
||||
- [Kubernetes][kubernetes] stores configuration data into etcd for service discovery and cluster management; etcd's consistency is crucial for correctly scheduling and operating services. The Kubernetes API server persists cluster state into etcd. It uses etcd's watch API to monitor the cluster and roll out critical configuration changes.
|
||||
|
||||
|
||||
## Features and system comparisons
|
||||
|
||||
TODO
|
||||
|
||||
[etcd-concurrency]: https://godoc.org/github.com/coreos/etcd/clientv3/concurrency
|
||||
[container-linux]: https://coreos.com/why
|
||||
[locksmith]: https://github.com/coreos/locksmith
|
||||
[kubernetes]: http://kubernetes.io/docs/whatisk8s
|
||||
|
@ -14,7 +14,6 @@
|
||||
- [etcdtool](https://github.com/mickep76/etcdtool) - Export/Import/Edit etcd directory as JSON/YAML/TOML and Validate directory using JSON schema
|
||||
- [etcd-rest](https://github.com/mickep76/etcd-rest) - Create generic REST API in Go using etcd as a backend with validation using JSON schema
|
||||
- [etcdsh](https://github.com/kamilhark/etcdsh) - A command line client with support of command history and tab completion. Supports v2
|
||||
- [etcdloadtest](https://github.com/sinsharat/etcdloadtest) - A command line load test client for etcd version 3.0 and above.
|
||||
|
||||
**Go libraries**
|
||||
|
||||
@ -24,7 +23,6 @@
|
||||
|
||||
**Java libraries**
|
||||
|
||||
- [coreos/jetcd](https://github.com/coreos/jetcd) - Supports v3
|
||||
- [boonproject/etcd](https://github.com/boonproject/boon/blob/master/etcd/README.md) - Supports v2, Async/Sync and waits
|
||||
- [justinsb/jetcd](https://github.com/justinsb/jetcd)
|
||||
- [diwakergupta/jetcd](https://github.com/diwakergupta/jetcd) - Supports v2
|
||||
@ -35,11 +33,9 @@
|
||||
**Scala libraries**
|
||||
|
||||
- [maciej/etcd-client](https://github.com/maciej/etcd-client) - Supports v2. Akka HTTP-based fully async client
|
||||
- [eiipii/etcdhttpclient](https://bitbucket.org/eiipii/etcdhttpclient) - Supports v2. Async HTTP client based on Netty and Scala Futures.
|
||||
|
||||
**Python libraries**
|
||||
|
||||
- [kragniz/python-etcd3](https://github.com/kragniz/python-etcd3) - Work in progress client for v3
|
||||
- [jplana/python-etcd](https://github.com/jplana/python-etcd) - Supports v2
|
||||
- [russellhaering/txetcd](https://github.com/russellhaering/txetcd) - a Twisted Python library
|
||||
- [cholcombe973/autodock](https://github.com/cholcombe973/autodock) - A docker deployment automation tool
|
||||
@ -65,8 +61,6 @@
|
||||
**C++ libraries**
|
||||
- [edwardcapriolo/etcdcpp](https://github.com/edwardcapriolo/etcdcpp) - Supports v2
|
||||
- [suryanathan/etcdcpp](https://github.com/suryanathan/etcdcpp) - Supports v2 (with waits)
|
||||
- [nokia/etcd-cpp-api](https://github.com/nokia/etcd-cpp-api) - Supports v2
|
||||
- [nokia/etcd-cpp-apiv3](https://github.com/nokia/etcd-cpp-apiv3) - Supports v3
|
||||
|
||||
**Clojure libraries**
|
||||
|
||||
@ -86,7 +80,6 @@
|
||||
**PHP Libraries**
|
||||
|
||||
- [linkorb/etcd-php](https://github.com/linkorb/etcd-php)
|
||||
- [activecollab/etcd](https://github.com/activecollab/etcd)
|
||||
|
||||
**Haskell libraries**
|
||||
|
||||
@ -96,10 +89,6 @@
|
||||
|
||||
- [ropensci/etseed](https://github.com/ropensci/etseed)
|
||||
|
||||
**Nim libraries**
|
||||
|
||||
- [etcd_client](https://github.com/FedericoCeratto/nim-etcd-client)
|
||||
|
||||
**Tcl libraries**
|
||||
|
||||
- [efrecon/etcd-tcl](https://github.com/efrecon/etcd-tcl) - Supports v2, except wait.
|
||||
@ -124,9 +113,7 @@
|
||||
**Projects using etcd**
|
||||
|
||||
- [binocarlos/yoda](https://github.com/binocarlos/yoda) - etcd + ZeroMQ
|
||||
- [blox/blox](https://github.com/blox/blox) - a collection of open source projects for container management and orchestration with AWS ECS
|
||||
- [calavera/active-proxy](https://github.com/calavera/active-proxy) - HTTP Proxy configured with etcd
|
||||
- [chain/chain](https://github.com/chain/chain) - software designed to operate and connect to highly scalable permissioned blockchain networks
|
||||
- [derekchiang/etcdplus](https://github.com/derekchiang/etcdplus) - A set of distributed synchronization primitives built upon etcd
|
||||
- [go-discover](https://github.com/flynn/go-discover) - service discovery in Go
|
||||
- [gleicon/goreman](https://github.com/gleicon/goreman/tree/etcd) - Branch of the Go Foreman clone with etcd support
|
||||
|
@ -70,8 +70,6 @@ All these metrics are prefixed with `etcd_network_`
|
||||
|---------------------------|--------------------------------------------------------------------|---------------|
|
||||
| peer_sent_bytes_total | The total number of bytes sent to the peer with ID `To`. | Counter(To) |
|
||||
| peer_received_bytes_total | The total number of bytes received from the peer with ID `From`. | Counter(From) |
|
||||
| peer_sent_failures_total | The total number of send failures from the peer with ID `To`. | Counter(To) |
|
||||
| peer_received_failures_total | The total number of receive failures from the peer with ID `From`. | Counter(From) |
|
||||
| peer_round_trip_time_seconds | Round-Trip-Time histogram between peers. | Histogram(To) |
|
||||
| client_grpc_sent_bytes_total | The total number of bytes sent to grpc clients. | Counter |
|
||||
| client_grpc_received_bytes_total| The total number of bytes received to grpc clients. | Counter |
|
||||
@ -82,7 +80,30 @@ All these metrics are prefixed with `etcd_network_`
|
||||
|
||||
### gRPC requests
|
||||
|
||||
These metrics are exposed via [go-grpc-prometheus][go-grpc-prometheus].
|
||||
These metrics describe the requests served by a specific etcd member: total received requests, total failed requests, and processing latency. They are useful for tracking user-generated traffic hitting the etcd cluster.
|
||||
|
||||
All these metrics are prefixed with `etcd_grpc_`
|
||||
|
||||
| Name | Description | Type |
|
||||
|--------------------------------|-------------------------------------------------------------------------------------|------------------------|
|
||||
| requests_total | Total number of received requests | Counter(method) |
|
||||
| requests_failed_total | Total number of failed requests. | Counter(method,error) |
|
||||
| unary_requests_duration_seconds | Bucketed handling duration of the requests. | Histogram(method) |
|
||||
|
||||
|
||||
Example Prometheus queries that may be useful from these metrics (across all etcd members):
|
||||
|
||||
* `sum(rate(etcd_grpc_requests_failed_total{job="etcd"}[1m]) by (grpc_method) / sum(rate(etcd_grpc_total{job="etcd"})[1m]) by (grpc_method)`
|
||||
|
||||
Shows the fraction of events that failed by gRPC method across all members, across a time window of `1m`.
|
||||
|
||||
* `sum(rate(etcd_grpc_requests_total{job="etcd",grpc_method="PUT"})[1m]) by (grpc_method)`
|
||||
|
||||
Shows the rate of PUT requests across all members, across a time window of `1m`.
|
||||
|
||||
* `histogram_quantile(0.9, sum(rate(etcd_grpc_unary_requests_duration_seconds{job="etcd",grpc_method="PUT"}[5m]) ) by (le))`
|
||||
|
||||
Show the 0.90-tile latency (in seconds) of PUT request handling across all members, with a window of `5m`.
|
||||
|
||||
## etcd_debugging namespace metrics
|
||||
|
||||
@ -113,4 +134,3 @@ Heavy file descriptor (`process_open_fds`) usage (i.e., near the process's file
|
||||
[prometheus-getting-started]: http://prometheus.io/docs/introduction/getting_started/
|
||||
[prometheus-naming]: http://prometheus.io/docs/practices/naming/
|
||||
[v2-http-metrics]: v2/metrics.md#http-requests
|
||||
[go-grpc-prometheus]: https://github.com/grpc-ecosystem/go-grpc-prometheus
|
@ -83,7 +83,7 @@ A cluster using self-signed certificates both encrypts traffic and authenticates
|
||||
On each machine, etcd would be started with these flags:
|
||||
|
||||
```
|
||||
$ etcd --name infra0 --initial-advertise-peer-urls https://10.0.1.10:2380 \
|
||||
$ etcd --name infra0 --initial-advertise-peer-urls http://10.0.1.10:2380 \
|
||||
--listen-peer-urls https://10.0.1.10:2380 \
|
||||
--listen-client-urls https://10.0.1.10:2379,https://127.0.0.1:2379 \
|
||||
--advertise-client-urls https://10.0.1.10:2379 \
|
||||
@ -126,7 +126,7 @@ $ etcd --name infra2 --initial-advertise-peer-urls https://10.0.1.12:2380 \
|
||||
|
||||
If the cluster needs encrypted communication but does not require authenticated connections, etcd can be configured to automatically generate its keys. On initialization, each member creates its own set of keys based on its advertised IP addresses and hosts.
|
||||
|
||||
On each machine, etcd would be started with these flags:
|
||||
On each machine, etcd would be started with these flag:
|
||||
|
||||
```
|
||||
$ etcd --name infra0 --initial-advertise-peer-urls https://10.0.1.10:2380 \
|
||||
@ -205,7 +205,7 @@ exit 1
|
||||
|
||||
## Discovery
|
||||
|
||||
In a number of cases, the IPs of the cluster peers may not be known ahead of time. This is common when utilizing cloud providers or when the network uses DHCP. In these cases, rather than specifying a static configuration, use an existing etcd cluster to bootstrap a new one. This process is called "discovery".
|
||||
In a number of cases, the IPs of the cluster peers may not be known ahead of time. This is common when utilizing cloud providers or when the network uses DHCP. In these cases, rather than specifying a static configuration, use an existing etcd cluster to bootstrap a new one. We call this process "discovery".
|
||||
|
||||
There two methods that can be used for discovery:
|
||||
|
||||
@ -214,17 +214,17 @@ There two methods that can be used for discovery:
|
||||
|
||||
### etcd discovery
|
||||
|
||||
To better understand the design of the discovery service protocol, we suggest reading the discovery service protocol [documentation][discovery-proto].
|
||||
To better understand the design about discovery service protocol, we suggest reading the discovery service protocol [documentation][discovery-proto].
|
||||
|
||||
#### Lifetime of a discovery URL
|
||||
|
||||
A discovery URL identifies a unique etcd cluster. Instead of reusing an existing discovery URL, each etcd instance shares a new discovery URL to bootstrap the new cluster.
|
||||
A discovery URL identifies a unique etcd cluster. Instead of reusing a discovery URL, always create discovery URLs for new clusters.
|
||||
|
||||
Moreover, discovery URLs should ONLY be used for the initial bootstrapping of a cluster. To change cluster membership after the cluster is already running, see the [runtime reconfiguration][runtime-conf] guide.
|
||||
|
||||
#### Custom etcd discovery service
|
||||
|
||||
Discovery uses an existing cluster to bootstrap itself. If using a private etcd cluster, create a URL like so:
|
||||
Discovery uses an existing cluster to bootstrap itself. If using a private etcd cluster, can create a URL like so:
|
||||
|
||||
```
|
||||
$ curl -X PUT https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83/_config/size -d value=3
|
||||
@ -271,7 +271,7 @@ $ curl https://discovery.etcd.io/new?size=3
|
||||
https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
```
|
||||
|
||||
This will create the cluster with an initial size of 3 members. If no size is specified, a default of 3 is used.
|
||||
This will create the cluster with an initial expected size of 3 members. If no size is specified, a default of 3 is used.
|
||||
|
||||
```
|
||||
ETCD_DISCOVERY=https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
@ -281,7 +281,7 @@ ETCD_DISCOVERY=https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573d
|
||||
--discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
```
|
||||
|
||||
**Each member must have a different name flag specified or else discovery will fail due to duplicated names. `Hostname` or `machine-id` can be a good choice. **
|
||||
**Each member must have a different name flag specified. `Hostname` or `machine-id` can be a good choice. Or discovery will fail due to duplicated name.**
|
||||
|
||||
Now we start etcd with those relevant flags for each member:
|
||||
|
||||
@ -456,10 +456,6 @@ $ etcd --name infra2 \
|
||||
--listen-peer-urls http://10.0.1.12:2380
|
||||
```
|
||||
|
||||
### Gateway
|
||||
|
||||
etcd gateway is a simple TCP proxy that forwards network data to the etcd cluster. Please read [gateway guide] for more information.
|
||||
|
||||
### Proxy
|
||||
|
||||
When the `--proxy` flag is set, etcd runs in [proxy mode][proxy]. This proxy mode only supports the etcd v2 API; there are no plans to support the v3 API. Instead, for v3 API support, there will be a new proxy with enhanced features following the etcd 3.0 release.
|
||||
@ -476,4 +472,3 @@ To setup an etcd cluster with proxies of v2 API, please read the the [clustering
|
||||
[clustering_etcd2]: https://github.com/coreos/etcd/blob/release-2.3/Documentation/clustering.md
|
||||
[security-guide]: security.md
|
||||
[tls-setup]: /hack/tls-setup
|
||||
[gateway]: gateway.md
|
||||
|
@ -247,7 +247,7 @@ The security flags help to [build a secure etcd cluster][security].
|
||||
+ env variable: ETCD_DEBUG
|
||||
|
||||
### --log-package-levels
|
||||
+ Set individual etcd subpackages to specific log levels. An example being `etcdserver=WARNING,security=DEBUG`
|
||||
+ Set individual etcd subpackages to specific log levels. An example being `etcdserver=WARNING,security=DEBUG`
|
||||
+ default: none (INFO for all packages)
|
||||
+ env variable: ETCD_LOG_PACKAGE_LEVELS
|
||||
|
||||
@ -276,13 +276,9 @@ Follow the instructions when using these flags.
|
||||
## Profiling flags
|
||||
|
||||
### --enable-pprof
|
||||
+ Enable runtime profiling data via HTTP server. Address is at client URL + "/debug/pprof/"
|
||||
+ Enable runtime profiling data via HTTP server. Address is at client URL + "/debug/pprof"
|
||||
+ default: false
|
||||
|
||||
### --metrics
|
||||
+ Set level of detail for exported metrics, specify 'extensive' to include histogram metrics.
|
||||
+ default: basic
|
||||
|
||||
[build-cluster]: clustering.md#static
|
||||
[reconfig]: runtime-configuration.md
|
||||
[discovery]: clustering.md#discovery
|
||||
|
@ -2,68 +2,6 @@
|
||||
|
||||
The following guide shows how to run etcd with rkt and Docker using the [static bootstrap process](clustering.md#static).
|
||||
|
||||
## rkt
|
||||
|
||||
### Running a single node etcd
|
||||
|
||||
The following rkt run command will expose the etcd client API on port 2379 and expose the peer API on port 2380.
|
||||
|
||||
Use the host IP address when configuring etcd.
|
||||
|
||||
```
|
||||
export NODE1=192.168.1.21
|
||||
```
|
||||
|
||||
Trust the CoreOS [App Signing Key](https://coreos.com/security/app-signing-key/).
|
||||
|
||||
```
|
||||
sudo rkt trust --prefix coreos.com/etcd
|
||||
# gpg key fingerprint is: 18AD 5014 C99E F7E3 BA5F 6CE9 50BD D3E0 FC8A 365E
|
||||
```
|
||||
|
||||
Run the `v3.0.6` version of etcd or specify another release version.
|
||||
|
||||
```
|
||||
sudo rkt run --net=default:IP=${NODE1} coreos.com/etcd:v3.0.6 -- -name=node1 -advertise-client-urls=http://${NODE1}:2379 -initial-advertise-peer-urls=http://${NODE1}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE1}:2380 -initial-cluster=node1=http://${NODE1}:2380
|
||||
```
|
||||
|
||||
List the cluster member.
|
||||
|
||||
```
|
||||
etcdctl --endpoints=http://192.168.1.21:2379 member list
|
||||
```
|
||||
|
||||
### Running a 3 node etcd cluster
|
||||
|
||||
Setup a 3 node cluster with rkt locally, using the `-initial-cluster` flag.
|
||||
|
||||
```sh
|
||||
export NODE1=172.16.28.21
|
||||
export NODE2=172.16.28.22
|
||||
export NODE3=172.16.28.23
|
||||
```
|
||||
|
||||
```
|
||||
# node 1
|
||||
sudo rkt run --net=default:IP=${NODE1} coreos.com/etcd:v3.0.6 -- -name=node1 -advertise-client-urls=http://${NODE1}:2379 -initial-advertise-peer-urls=http://${NODE1}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE1}:2380 -initial-cluster=node1=http://${NODE1}:2380,node2=http://${NODE2}:2380,node3=http://${NODE3}:2380
|
||||
|
||||
# node 2
|
||||
sudo rkt run --net=default:IP=${NODE2} coreos.com/etcd:v3.0.6 -- -name=node2 -advertise-client-urls=http://${NODE2}:2379 -initial-advertise-peer-urls=http://${NODE2}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE2}:2380 -initial-cluster=node1=http://${NODE1}:2380,node2=http://${NODE2}:2380,node3=http://${NODE3}:2380
|
||||
|
||||
# node 3
|
||||
sudo rkt run --net=default:IP=${NODE3} coreos.com/etcd:v3.0.6 -- -name=node3 -advertise-client-urls=http://${NODE3}:2379 -initial-advertise-peer-urls=http://${NODE3}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE3}:2380 -initial-cluster=node1=http://${NODE1}:2380,node2=http://${NODE2}:2380,node3=http://${NODE3}:2380
|
||||
```
|
||||
|
||||
Verify the cluster is healthy and can be reached.
|
||||
|
||||
```
|
||||
ETCDCTL_API=3 etcdctl --endpoints=http://172.16.28.21:2379,http://172.16.28.22:2379,http://172.16.28.23:2379 endpoint health
|
||||
```
|
||||
|
||||
### DNS
|
||||
|
||||
Production clusters which refer to peers by DNS name known to the local resolver must mount the [host's DNS configuration](https://coreos.com/kubernetes/docs/latest/kubelet-wrapper.html#customizing-rkt-options).
|
||||
|
||||
## Docker
|
||||
|
||||
In order to expose the etcd API to clients outside of Docker host, use the host IP address of the container. Please see [`docker inspect`](https://docs.docker.com/engine/reference/commandline/inspect) for more detail on how to get the IP address. Alternatively, specify `--net=host` flag to `docker run` command to skip placing the container inside of a separate network stack.
|
||||
@ -121,7 +59,3 @@ To run `etcdctl` using API version 3:
|
||||
docker exec etcd /bin/sh -c "export ETCDCTL_API=3 && /usr/local/bin/etcdctl put foo bar"
|
||||
```
|
||||
|
||||
## Bare Metal
|
||||
|
||||
To provision a 3 node etcd cluster on bare-metal, you might find the examples in the [baremetal repo](https://github.com/coreos/coreos-baremetal/tree/master/examples) useful.
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 96 KiB |
@ -1,66 +0,0 @@
|
||||
# etcd gateway
|
||||
|
||||
## What is etcd gateway
|
||||
|
||||
etcd gateway is a simple TCP proxy that forwards network data to the etcd cluster. The gateway is stateless and transparent; it neither inspects client requests nor interferes with cluster responses.
|
||||
|
||||
The gateway supports multiple etcd server endpoints. When the gateway starts, it randomly picks one etcd server endpoint and forwards all requests to that endpoint. This endpoint serves all requests until the gateway detects a network failure. If the gateway detects an endpoint failure, it will switch to a different endpoint, if available, to hide failures from its clients. Other retry policies, such as weighted round-robin, may be supported in the future.
|
||||
|
||||
## When to use etcd gateway
|
||||
|
||||
Every application that accesses etcd must first have the address of an etcd cluster client endpoint. If multiple applications on the same server access the same etcd cluster, every application still needs to know the advertised client endpoints of the etcd cluster. If the etcd cluster is reconfigured to have different endpoints, every application may also need to update its endpoint list. This wide-scale reconfiguration is both tedious and error prone.
|
||||
|
||||
etcd gateway solves this problem by serving as a stable local endpoint. A typical etcd gateway configuration has
|
||||
each machine running a gateway listening on a local address and every etcd application connecting to its local gateway. The upshot is only the gateway needs to update its endpoints instead of updating each and every application.
|
||||
|
||||
In summary, to automatically propagate cluster endpoint changes, the etcd gateway runs on every machine serving multiple applications accessing same etcd cluster.
|
||||
|
||||
## When not to use etcd gateway
|
||||
|
||||
- Improving performance
|
||||
|
||||
The gateway is not designed for improving etcd cluster performance. It does not provide caching, watch coalescing or batching. The etcd team is developing a caching proxy designed for improving cluster scalability.
|
||||
|
||||
- Running on a cluster management system
|
||||
|
||||
Advanced cluster management systems like Kubernetes natively support service discovery. Applications can access an etcd cluster with a DNS name or a virtual IP address managed by the system. For example, kube-proxy is equivalent to etcd gateway.
|
||||
|
||||
## Start etcd gateway
|
||||
|
||||
Consider an etcd cluster with the following static endpoints:
|
||||
|
||||
|Name|Address|Hostname|
|
||||
|------|---------|------------------|
|
||||
|infra0|10.0.1.10|infra0.example.com|
|
||||
|infra1|10.0.1.11|infra1.example.com|
|
||||
|infra2|10.0.1.12|infra2.example.com|
|
||||
|
||||
Start the etcd gateway to use these static endpoints with the command:
|
||||
|
||||
```bash
|
||||
$ etcd gateway start --endpoints=infra0.example.com,infra1.example.com,infra2.example.com
|
||||
2016-08-16 11:21:18.867350 I | tcpproxy: ready to proxy client requests to [...]
|
||||
```
|
||||
|
||||
Alternatively, if using DNS for service discovery, consider the DNS SRV entries:
|
||||
|
||||
```bash
|
||||
$ dig +noall +answer SRV _etcd-client._tcp.example.com
|
||||
_etcd-client._tcp.example.com. 300 IN SRV 0 0 2379 infra0.example.com.
|
||||
_etcd-client._tcp.example.com. 300 IN SRV 0 0 2379 infra1.example.com.
|
||||
_etcd-client._tcp.example.com. 300 IN SRV 0 0 2379 infra2.example.com.
|
||||
```
|
||||
|
||||
```bash
|
||||
$ dig +noall +answer infra0.example.com infra1.example.com infra2.example.com
|
||||
infra0.example.com. 300 IN A 10.0.1.10
|
||||
infra1.example.com. 300 IN A 10.0.1.11
|
||||
infra2.example.com. 300 IN A 10.0.1.12
|
||||
```
|
||||
|
||||
Start the etcd gateway to fetch the endpoints from the DNS SRV entries with the command:
|
||||
|
||||
```bash
|
||||
$ etcd gateway --discovery-srv=example.com
|
||||
2016-08-16 11:21:18.867350 I | tcpproxy: ready to proxy client requests to [...]
|
||||
```
|
File diff suppressed because it is too large
Load Diff
@ -1,77 +0,0 @@
|
||||
# gRPC proxy
|
||||
|
||||
*This is an alpha feature, we are looking for early feedback.*
|
||||
|
||||
The gRPC proxy is a stateless etcd reverse proxy operating at the gRPC layer (L7). The proxy is designed to reduce the total processing load on the core etcd cluster. For horizontal scalability, it coalesces watch and lease API requests. To protect the cluster against abusive clients, it caches key range requests.
|
||||
|
||||
The gRPC proxy supports multiple etcd server endpoints. When the proxy starts, it randomly picks one etcd server endpoint to use. This endpoint serves all requests until the proxy detects an endpoint failure. If the gRPC proxy detects an endpoint failure, it switches to a different endpoint, if available, to hide failures from its clients. Other retry policies, such as weighted round-robin, may be supported in the future.
|
||||
|
||||
## Scalable watch API
|
||||
|
||||
The gRPC proxy coalesces multiple client watchers (`c-watchers`) on the same key or range into a single watcher (`s-watcher`) connected to an etcd server. The proxy broadcasts all events from the `s-watcher` to its `c-watchers`.
|
||||
|
||||
Assuming N clients watch the same key, one gRPC proxy can reduce the watch load on the etcd server from N to 1. Users can deploy multiple gRPC proxies to further distribute server load.
|
||||
|
||||
In the following example, three clients watch on key A. The gRPC proxy coalesces the three watchers, creating a single watcher attached to the etcd server.
|
||||
|
||||
```
|
||||
+-------------+
|
||||
| etcd server |
|
||||
+------+------+
|
||||
^ watch key A (s-watcher)
|
||||
|
|
||||
+-------+-----+
|
||||
| gRPC proxy | <-------+
|
||||
| | |
|
||||
++-----+------+ |watch key A (c-watcher)
|
||||
watch key A ^ ^ watch key A |
|
||||
(c-watcher) | | (c-watcher) |
|
||||
+-------+-+ ++--------+ +----+----+
|
||||
| client | | client | | client |
|
||||
| | | | | |
|
||||
+---------+ +---------+ +---------+
|
||||
```
|
||||
|
||||
### Limitations
|
||||
|
||||
To effectively coalesce multiple client watchers into a single watcher, the gRPC proxy coalesces new `c-watchers` into an existing `s-watcher` when possible. This coalesced `s-watcher` may be out of sync with the etcd server due to network delays or buffered undelivered events. When the watch revision is unspecified, the gRPC proxy will not guarantee the `c-watcher` will start watching from the most recent store revision. For example, if a client watches from an etcd server with revision 1000, that watcher will begin at revision 1000. If a client watches from the gRPC proxy, may begin watching from revision 990.
|
||||
|
||||
Similar limitations apply to cancellation. When the watcher is cancelled, the etcd server’s revision may be greater than the cancellation response revision.
|
||||
|
||||
These two limitations should not cause problems for most use cases. In the future, there may be additional options to force the watcher to bypass the gRPC proxy for more accurate revision responses.
|
||||
|
||||
## Scalable lease API
|
||||
|
||||
TODO
|
||||
|
||||
## Abusive clients protection
|
||||
|
||||
The gRPC proxy caches responses for requests when it does not break consistency requirements. This can protect the etcd server from abusive clients in tight for loops.
|
||||
|
||||
## Start etcd gRPC proxy
|
||||
|
||||
Consider an etcd cluster with the following static endpoints:
|
||||
|
||||
|Name|Address|Hostname|
|
||||
|------|---------|------------------|
|
||||
|infra0|10.0.1.10|infra0.example.com|
|
||||
|infra1|10.0.1.11|infra1.example.com|
|
||||
|infra2|10.0.1.12|infra2.example.com|
|
||||
|
||||
Start the etcd gRPC proxy to use these static endpoints with the command:
|
||||
|
||||
```bash
|
||||
$ etcd grpc-proxy start --endpoints=infra0.example.com,infra1.example.com,infra2.example.com --listen-addr=127.0.0.1:2379
|
||||
```
|
||||
|
||||
The etcd gRPC proxy starts and listens on port 8080. It forwards client requests to one of the three endpoints provided above.
|
||||
|
||||
Sending requests through the proxy:
|
||||
|
||||
```bash
|
||||
$ ETCDCTL_API=3 ./etcdctl --endpoints=127.0.0.1:2379 put foo bar
|
||||
OK
|
||||
$ ETCDCTL_API=3 ./etcdctl --endpoints=127.0.0.1:2379 get foo
|
||||
foo
|
||||
bar
|
||||
```
|
@ -1,93 +0,0 @@
|
||||
# Hardware recommendations
|
||||
|
||||
etcd usually runs well with limited resources for development or testing purposes; it’s common to develop with etcd on a laptop or a cheap cloud machine. However, when running etcd clusters in production, some hardware guidelines are useful for proper administration. These suggestions are not hard rules; they serve as a good starting point for a robust production deployment. As always, deployments should be tested with simulated workloads before running in production.
|
||||
|
||||
## CPUs
|
||||
|
||||
Few etcd deployments require a lot of CPU capacity. Typical clusters need two to four cores to run smoothly.
|
||||
Heavily loaded etcd deployments, serving thousands of clients or tens of thousands of requests per second, tend to be CPU bound since etcd can serve requests from memory. Such heavy deployments usually need eight to sixteen dedicated cores.
|
||||
|
||||
|
||||
## Memory
|
||||
|
||||
etcd has a relatively small memory footprint but its performance still depends on having enough memory. An etcd server will aggressively cache key-value data and spends most of the rest of its memory tracking watchers. Typically 8GB is enough. For heavy deployments with thousands of watchers and millions of keys, allocate 16GB to 64GB memory accordingly.
|
||||
|
||||
|
||||
## Disks
|
||||
|
||||
Fast disks are the most critical factor for etcd deployment performance and stability.
|
||||
|
||||
A slow disk will increase etcd request latency and potentially hurt cluster stability. Since etcd’s consensus protocol depends on persistently storing metadata to a log, a majority of etcd cluster members must write every request down to disk. Additionally, etcd will also incrementally checkpoint its state to disk so it can truncate this log. If these writes take too long, heartbeats may time out and trigger an election, undermining the stability of the cluster.
|
||||
|
||||
etcd is very sensitive to disk write latency. Typically 50 sequential IOPS (e.g., a 7200 RPM disk) is required. For heavily loaded clusters, 500 sequential IOPS (e.g., a typical local SSD or a high performance virtualized block device) is recommended. Note that most cloud providers publish concurrent IOPS rather than sequential IOPS; the published concurrent IOPS can be 10x greater than the sequential IOPS. To measure actual sequential IOPS, we suggest using a disk benchmarking tool such as [diskbench][diskbench] or [fio][fio].
|
||||
|
||||
etcd requires only modest disk bandwidth but more disk bandwidth buys faster recovery times when a failed member has to catch up with the cluster. Typically 10MB/s will recover 100MB data within 15 seconds. For large clusters, 100MB/s or higher is suggested for recovering 1GB data within 15 seconds.
|
||||
|
||||
When possible, back etcd’s storage with a SSD. A SSD usually provides lower write latencies and with less variance than a spinning disk, thus improving the stability and reliability of etcd. If using spinning disk, get the fastest disks possible (15,000 RPM). Using RAID 0 is also an effective way to increase disk speed, for both spinning disks and SSD. With at least three cluster members, mirroring and/or parity variants of RAID are unnecessary; etcd's consistent replication already gets high availability.
|
||||
|
||||
|
||||
## Network
|
||||
|
||||
Multi-member etcd deployments benefit from a fast and reliable network. In order for etcd to be both consistent and partition tolerant, an unreliable network with partitioning outages will lead to poor availability. Low latency ensures etcd members can communicate fast. High bandwidth can reduce the time to recover a failed etcd member. 1GbE is sufficient for common etcd deployments. For large etcd clusters, a 10GbE network will reduce mean time to recovery.
|
||||
|
||||
Deploy etcd members within a single data center when possible to avoid latency overheads and lessen the possibility of partitioning events. If a failure domain in another data center is required, choose a data center closer to the existing one. Please also read the [tuning][tuning] documentation for more information on cross data center deployment.
|
||||
|
||||
|
||||
## Example hardware configurations
|
||||
|
||||
Here are a few example hardware setups on AWS and GCE environments. As mentioned before, but must be stressed regardless, administrators should test an etcd deployment with a simulated workload before putting it into production.
|
||||
|
||||
Note that these configurations assume these machines are totally dedicated to etcd. Running other applications along with etcd on these machines may cause resource contentions and lead to cluster instability.
|
||||
|
||||
### Small cluster
|
||||
|
||||
A small cluster serves fewer than 100 clients, fewer than 200 of requests per second, and stores no more than 100MB of data.
|
||||
|
||||
Example application workload: A 50-node Kubernetes cluster
|
||||
|
||||
| Provider | Type | vCPUs | Memory (GB) | Max concurrent IOPS | Disk bandwidth (MB/s) |
|
||||
|----------|------|-------|--------|------|----------------|
|
||||
| AWS | m4.large | 2 | 8 | 3600 | 56.25 |
|
||||
| GCE | n1-standard-1 + 50GB PD SSD | 2 | 7.5 | 1500 | 25 |
|
||||
|
||||
|
||||
### Medium cluster
|
||||
|
||||
A medium cluster serves fewer than 500 clients, fewer than 1,000 of requests per second, and stores no more than 500MB of data.
|
||||
|
||||
Example application workload: A 250-node Kubernetes cluster
|
||||
|
||||
| Provider | Type | vCPUs | Memory (GB) | Max concurrent IOPS | Disk bandwidth (MB/s) |
|
||||
|----------|------|-------|--------|------|----------------|
|
||||
| AWS | m4.xlarge | 4 | 16 | 6000 | 93.75 |
|
||||
| GCE | n1-standard-4 + 150GB PD SSD | 4 | 15 | 4500 | 75 |
|
||||
|
||||
|
||||
### Large cluster
|
||||
|
||||
A large cluster serves fewer than 1,500 clients, fewer than 10,000 of requests per second, and stores no more than 1GB of data.
|
||||
|
||||
Example application workload: A 1,000-node Kubernetes cluster
|
||||
|
||||
| Provider | Type | vCPUs | Memory (GB) | Max concurrent IOPS | Disk bandwidth (MB/s) |
|
||||
|----------|------|-------|--------|------|----------------|
|
||||
| AWS | m4.2xlarge | 8 | 32 | 8000 | 125 |
|
||||
| GCE | n1-standard-8 + 250GB PD SSD | 8 | 30 | 7500 | 125 |
|
||||
|
||||
|
||||
### xLarge cluster
|
||||
|
||||
An xLarge cluster serves more than 1,500 clients, more than 10,000 of requests per second, and stores more than 1GB data.
|
||||
|
||||
Example application workload: A 3,000 node Kubernetes cluster
|
||||
|
||||
| Provider | Type | vCPUs | Memory (GB) | Max concurrent IOPS | Disk bandwidth (MB/s) |
|
||||
|----------|------|-------|--------|------|----------------|
|
||||
| AWS | m4.4xlarge | 16 | 64 | 16,000 | 250 |
|
||||
| GCE | n1-standard-16 + 500GB PD SSD | 16 | 60 | 15,000 | 250 |
|
||||
|
||||
|
||||
[diskbench]: https://github.com/ongardie/diskbenchmark
|
||||
[fio]: https://github.com/axboe/fio
|
||||
[tuning]: ../tuning.md
|
||||
|
@ -49,50 +49,51 @@ Finished defragmenting etcd member[127.0.0.1:2379]
|
||||
|
||||
## Space quota
|
||||
|
||||
The space quota in `etcd` ensures the cluster operates in a reliable fashion. Without a space quota, `etcd` may suffer from poor performance if the keyspace grows excessively large, or it may simply run out of storage space, leading to unpredictable cluster behavior. If the keyspace's backend database for any member exceeds the space quota, `etcd` raises a cluster-wide alarm that puts the cluster into a maintenance mode which only accepts key reads and deletes. Only after freeing enough space in the keyspace and defragmenting the backend database, along with clearing the space quota alarm can the cluster resume normal operation.
|
||||
The space quota in `etcd` ensures the cluster operates in a reliable fashion. Without a space quota, `etcd` may suffer from poor performance if the keyspace grows excessively large, or it may simply run out of storage space, leading to unpredictable cluster behavior. If the keyspace's backend database for any member exceeds the space quota, `etcd` raises a cluster-wide alarm that puts the cluster into a maintenance mode which only accepts key reads and deletes. After freeing enough space in the keyspace, the alarm can be disarmed and the cluster will resume normal operation.
|
||||
|
||||
By default, `etcd` sets a conservative space quota suitable for most applications, but it may be configured on the command line, in bytes:
|
||||
|
||||
```sh
|
||||
# set a very small 16MB quota
|
||||
$ etcd --quota-backend-bytes=$((16*1024*1024))
|
||||
$ etcd --quota-backend-bytes=16777216
|
||||
```
|
||||
|
||||
The space quota can be triggered with a loop:
|
||||
|
||||
```sh
|
||||
# fill keyspace
|
||||
$ while [ 1 ]; do dd if=/dev/urandom bs=1024 count=1024 | ETCDCTL_API=3 etcdctl put key || break; done
|
||||
$ while [ 1 ]; do dd if=/dev/urandom bs=1024 count=1024 | etcdctl put key || break; done
|
||||
...
|
||||
Error: rpc error: code = 8 desc = etcdserver: mvcc: database space exceeded
|
||||
# confirm quota space is exceeded
|
||||
$ ETCDCTL_API=3 etcdctl --write-out=table endpoint status
|
||||
$ etcdctl --write-out=table endpoint status
|
||||
+----------------+------------------+-----------+---------+-----------+-----------+------------+
|
||||
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | RAFT TERM | RAFT INDEX |
|
||||
+----------------+------------------+-----------+---------+-----------+-----------+------------+
|
||||
| 127.0.0.1:2379 | bf9071f4639c75cc | 2.3.0+git | 18 MB | true | 2 | 3332 |
|
||||
+----------------+------------------+-----------+---------+-----------+-----------+------------+
|
||||
# confirm alarm is raised
|
||||
$ ETCDCTL_API=3 etcdctl alarm list
|
||||
$ etcdctl alarm list
|
||||
memberID:13803658152347727308 alarm:NOSPACE
|
||||
```
|
||||
|
||||
Removing excessive keyspace data and defragmenting the backend database will put the cluster back within the quota limits:
|
||||
Removing excessive keyspace data will put the cluster back within the quota limits so the alarm can be disarmed:
|
||||
|
||||
```sh
|
||||
# get current revision
|
||||
$ rev=$(ETCDCTL_API=3 etcdctl --endpoints=:2379 endpoint status --write-out="json" | egrep -o '"revision":[0-9]*' | egrep -o '[0-9]*')
|
||||
$ etcdctl --endpoints=:2379 endpoint status
|
||||
[{"Endpoint":"127.0.0.1:2379","Status":{"header":{"cluster_id":8925027824743593106,"member_id":13803658152347727308,"revision":1516,"raft_term":2},"version":"2.3.0+git","dbSize":17973248,"leader":13803658152347727308,"raftIndex":6359,"raftTerm":2}}]
|
||||
# compact away all old revisions
|
||||
$ ETCDCTL_API=3 etcdctl compact $rev
|
||||
$ etdctl compact 1516
|
||||
compacted revision 1516
|
||||
# defragment away excessive space
|
||||
$ ETCDCTL_API=3 etcdctl defrag
|
||||
$ etcdctl defrag
|
||||
Finished defragmenting etcd member[127.0.0.1:2379]
|
||||
# disarm alarm
|
||||
$ ETCDCTL_API=3 etcdctl alarm disarm
|
||||
$ etcdctl alarm disarm
|
||||
memberID:13803658152347727308 alarm:NOSPACE
|
||||
# test puts are allowed again
|
||||
$ ETCDCTL_API=3 etcdctl put newkey 123
|
||||
$ etdctl put newkey 123
|
||||
OK
|
||||
```
|
||||
|
||||
|
@ -1,82 +0,0 @@
|
||||
# Monitoring etcd
|
||||
|
||||
Each etcd server exports metrics under the `/metrics` path on its client port.
|
||||
|
||||
The metrics can be fetched with `curl`:
|
||||
|
||||
```sh
|
||||
$ curl -L http://localhost:2379/metrics
|
||||
|
||||
# HELP etcd_debugging_mvcc_keys_total Total number of keys.
|
||||
# TYPE etcd_debugging_mvcc_keys_total gauge
|
||||
etcd_debugging_mvcc_keys_total 0
|
||||
# HELP etcd_debugging_mvcc_pending_events_total Total number of pending events to be sent.
|
||||
# TYPE etcd_debugging_mvcc_pending_events_total gauge
|
||||
etcd_debugging_mvcc_pending_events_total 0
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
## Prometheus
|
||||
|
||||
Running a [Prometheus][prometheus] monitoring service is the easiest way to ingest and record etcd's metrics.
|
||||
|
||||
First, install Prometheus:
|
||||
|
||||
```sh
|
||||
PROMETHEUS_VERSION="1.3.1"
|
||||
wget https://github.com/prometheus/prometheus/releases/download/v$PROMETHEUS_VERSION/prometheus-$PROMETHEUS_VERSION.linux-amd64.tar.gz -O /tmp/prometheus-$PROMETHEUS_VERSION.linux-amd64.tar.gz
|
||||
tar -xvzf /tmp/prometheus-$PROMETHEUS_VERSION.linux-amd64.tar.gz --directory /tmp/ --strip-components=1
|
||||
/tmp/prometheus -version
|
||||
```
|
||||
|
||||
Set Prometheus's scraper to target the etcd cluster endpoints:
|
||||
|
||||
```sh
|
||||
cat > /tmp/test-etcd.yaml <<EOF
|
||||
global:
|
||||
scrape_interval: 10s
|
||||
scrape_configs:
|
||||
- job_name: test-etcd
|
||||
static_configs:
|
||||
- targets: ['10.240.0.32:2379','10.240.0.33:2379','10.240.0.34:2379']
|
||||
EOF
|
||||
cat /tmp/test-etcd.yaml
|
||||
```
|
||||
|
||||
Set up the Prometheus handler:
|
||||
|
||||
```sh
|
||||
nohup /tmp/prometheus \
|
||||
-config.file /tmp/test-etcd.yaml \
|
||||
-web.listen-address ":9090" \
|
||||
-storage.local.path "test-etcd.data" >> /tmp/test-etcd.log 2>&1 &
|
||||
```
|
||||
|
||||
Now Prometheus will scrape etcd metrics every 10 seconds.
|
||||
|
||||
|
||||
## Grafana
|
||||
|
||||
[Grafana][grafana] has built-in Prometheus support; just add a Prometheus data source:
|
||||
|
||||
```
|
||||
Name: test-etcd
|
||||
Type: Prometheus
|
||||
Url: http://localhost:9090
|
||||
Access: proxy
|
||||
```
|
||||
|
||||
Then import the default [etcd dashboard template][template] and customize. For instance, if Prometheus data source name is `my-etcd`, the `datasource` field values in JSON also need to be `my-etcd`.
|
||||
|
||||
See the [demo][demo].
|
||||
|
||||
Sample dashboard:
|
||||
|
||||

|
||||
|
||||
|
||||
[prometheus]: https://prometheus.io/
|
||||
[grafana]: http://grafana.org/
|
||||
[template]: ./grafana.json
|
||||
[demo]: http://dash.etcd.io/dashboard/db/test-etcd
|
@ -11,7 +11,7 @@ To recover from disastrous failure, etcd v3 provides snapshot and restore facili
|
||||
Recovering a cluster first needs a snapshot of the keyspace from an etcd member. A snapshot may either be taken from a live member with the `etcdctl snapshot save` command or by copying the `member/snap/db` file from an etcd data directory. For example, the following command snapshots the keyspace served by `$ENDPOINT` to the file `snapshot.db`:
|
||||
|
||||
```sh
|
||||
$ ETCDCTL_API=3 etcdctl --endpoints $ENDPOINT snapshot save snapshot.db
|
||||
$ etcdctl --endpoints $ENDPOINT snapshot save snapshot.db
|
||||
```
|
||||
|
||||
### Restoring a cluster
|
||||
@ -23,19 +23,19 @@ Snapshot integrity may be optionally verified at restore time. If the snapshot i
|
||||
A restore initializes a new member of a new cluster, with a fresh cluster configuration using `etcd`'s cluster configuration flags, but preserves the contents of the etcd keyspace. Continuing from the previous example, the following creates new etcd data directories (`m1.etcd`, `m2.etcd`, `m3.etcd`) for a three member cluster:
|
||||
|
||||
```sh
|
||||
$ ETCDCTL_API=3 etcdctl snapshot restore snapshot.db \
|
||||
$ etcdctl snapshot restore snapshot.db \
|
||||
--name m1 \
|
||||
--initial-cluster m1=http://host1:2380,m2=http://host2:2380,m3=http://host3:2380 \
|
||||
--initial-cluster m1=http:/host1:2380,m2=http://host2:2380,m3=http://host3:2380 \
|
||||
--initial-cluster-token etcd-cluster-1 \
|
||||
--initial-advertise-peer-urls http://host1:2380
|
||||
$ ETCDCTL_API=3 etcdctl snapshot restore snapshot.db \
|
||||
$ etcdctl snapshot restore snapshot.db \
|
||||
--name m2 \
|
||||
--initial-cluster m1=http://host1:2380,m2=http://host2:2380,m3=http://host3:2380 \
|
||||
--initial-cluster m1=http:/host1:2380,m2=http://host2:2380,m3=http://host3:2380 \
|
||||
--initial-cluster-token etcd-cluster-1 \
|
||||
--initial-advertise-peer-urls http://host2:2380
|
||||
$ ETCDCTL_API=3 etcdctl snapshot restore snapshot.db \
|
||||
$ etcdctl snapshot restore snapshot.db \
|
||||
--name m3 \
|
||||
--initial-cluster m1=http://host1:2380,m2=http://host2:2380,m3=http://host3:2380 \
|
||||
--initial-cluster m1=http:/host1:2380,m2=http://host2:2380,m3=http://host3:2380 \
|
||||
--initial-cluster-token etcd-cluster-1 \
|
||||
--initial-advertise-peer-urls http://host3:2380
|
||||
```
|
||||
|
@ -169,7 +169,7 @@ As described in the above, the best practice of adding new members is to configu
|
||||
|
||||
For avoiding this problem, etcd provides an option `-strict-reconfig-check`. If this option is passed to etcd, etcd rejects reconfiguration requests if the number of started members will be less than a quorum of the reconfigured cluster.
|
||||
|
||||
It is enabled by default.
|
||||
It is recommended to enable this option. However, it is disabled by default because of keeping compatibility.
|
||||
|
||||
[add member]: #add-a-new-member
|
||||
[cluster-reconf]: #cluster-reconfiguration-operations
|
||||
|
@ -1,39 +1,14 @@
|
||||
## Supported platforms
|
||||
|
||||
### Current support
|
||||
|
||||
The following table lists etcd support status for common architectures and operating systems,
|
||||
|
||||
| Architecture | Operating System | Status | Maintainers |
|
||||
| ------------ | ---------------- | ------------ | ---------------- |
|
||||
| amd64 | Darwin | Experimental | etcd maintainers |
|
||||
| amd64 | Linux | Stable | etcd maintainers |
|
||||
| amd64 | Windows | Experimental | |
|
||||
| arm64 | Linux | Experimental | @glevand |
|
||||
| arm | Linux | Unstable | |
|
||||
| 386 | Linux | Unstable | |
|
||||
|
||||
* etcd-maintainers are listed in https://github.com/coreos/etcd/blob/master/MAINTAINERS.
|
||||
|
||||
Experimental platforms appear to work in practice and have some platform specific code in etcd, but do not fully conform to the stable support policy. Unstable platforms have been lightly tested, but less than experimental. Unlisted architecture and operating system pairs are currently unsupported; caveat emptor.
|
||||
|
||||
### Supporting a new platform
|
||||
|
||||
For etcd to officially support a new platform as stable, a few requirements are necessary to ensure acceptable quality:
|
||||
|
||||
1. An "official" maintainer for the platform with clear motivation; someone must be responsible for taking care of the platform.
|
||||
2. Set up CI for build; etcd must compile.
|
||||
3. Set up CI for running unit tests; etcd must pass simple tests.
|
||||
4. Set up CI (TravisCI, SemaphoreCI or Jenkins) for running integration tests; etcd must pass intensive tests.
|
||||
5. (Optional) Set up a functional testing cluster; an etcd cluster should survive stress testing.
|
||||
## Supported platform
|
||||
|
||||
### 32-bit and other unsupported systems
|
||||
|
||||
etcd has known issues on 32-bit systems due to a bug in the Go runtime. See the [Go issue][go-issue] and [atomic package][go-atomic] for more information.
|
||||
etcd has known issues on 32-bit systems due to a bug in the Go runtime. See #[358][358] for more information.
|
||||
|
||||
To avoid inadvertently running a possibly unstable etcd server, `etcd` on unstable or unsupported architectures will print a warning message and immediately exit if the environment variable `ETCD_UNSUPPORTED_ARCH` is not set to the target architecture.
|
||||
To avoid inadvertently running a possibly unstable etcd server, `etcd` on unsupported architectures will print
|
||||
a warning message and immediately exit if the environment variable `ETCD_UNSUPPORTED_ARCH` is not set to
|
||||
the target architecture.
|
||||
|
||||
Currently only the amd64 architecture is officially supported by `etcd`.
|
||||
|
||||
[go-issue]: https://github.com/golang/go/issues/599
|
||||
[go-atomic]: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
||||
[358]: https://github.com/coreos/etcd/issues/358
|
||||
|
||||
|
@ -71,23 +71,4 @@ $ etcd --snapshot-count=5000
|
||||
$ ETCD_SNAPSHOT_COUNT=5000 etcd
|
||||
```
|
||||
|
||||
## Network
|
||||
|
||||
If the etcd leader serves a large number of concurrent client requests, it may delay processing follower peer requests due to network congestion. This manifests as send buffer error messages on the follower nodes:
|
||||
|
||||
```
|
||||
dropped MsgProp to 247ae21ff9436b2d since streamMsg's sending buffer is full
|
||||
dropped MsgAppResp to 247ae21ff9436b2d since streamMsg's sending buffer is full
|
||||
```
|
||||
|
||||
These errors may be resolved by prioritizing etcd's peer traffic over its client traffic. On Linux, peer traffic can be prioritized by using the traffic control mechanism:
|
||||
|
||||
```
|
||||
tc qdisc add dev eth0 root handle 1: prio bands 3
|
||||
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 2380 0xffff flowid 1:1
|
||||
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dport 2380 0xffff flowid 1:1
|
||||
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip sport 2739 0xffff flowid 1:1
|
||||
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip dport 2739 0xffff flowid 1:1
|
||||
```
|
||||
|
||||
[ping]: https://en.wikipedia.org/wiki/Ping_(networking_utility)
|
||||
|
@ -6,27 +6,27 @@ In the general case, upgrading from etcd 2.3 to 3.0 can be a zero-downtime, roll
|
||||
|
||||
Before [starting an upgrade](#upgrade-procedure), read through the rest of this guide to prepare.
|
||||
|
||||
### Upgrade checklists
|
||||
### Upgrade Checklists
|
||||
|
||||
#### Upgrade requirements
|
||||
#### Upgrade Requirements
|
||||
|
||||
To upgrade an existing etcd deployment to 3.0, the running cluster must be 2.3 or greater. If it's before 2.3, please upgrade to [2.3](https://github.com/coreos/etcd/releases/tag/v2.3.0) before upgrading to 3.0.
|
||||
|
||||
Also, to ensure a smooth rolling upgrade, the running cluster must be healthy. Check the health of the cluster by using the `etcdctl cluster-health` command before proceeding.
|
||||
Also, to ensure a smooth rolling upgrade, the running cluster must be healthy. You can check the health of the cluster by using the `etcdctl cluster-health` command.
|
||||
|
||||
#### Preparation
|
||||
|
||||
Before upgrading etcd, always test the services relying on etcd in a staging environment before deploying the upgrade to the production environment.
|
||||
|
||||
Before beginning, [backup the etcd data directory](../v2/admin_guide.md#backing-up-the-datastore). Should something go wrong with the upgrade, it is possible to use this backup to [downgrade](#downgrade) back to existing etcd version.
|
||||
Before beginning, [backup the etcd data directory](../v2/admin_guide.md#backing-up-the-datastore). Should something go wrong with the upgrade, it is possible to use this backup to [downgrade](#downgrade) back to existing etcd version.
|
||||
|
||||
#### Mixed versions
|
||||
#### Mixed Versions
|
||||
|
||||
While upgrading, an etcd cluster supports mixed versions of etcd members, and operates with the protocol of the lowest common version. The cluster is only considered upgraded once all of its members are upgraded to version 3.0. Internally, etcd members negotiate with each other to determine the overall cluster version, which controls the reported version and the supported features.
|
||||
|
||||
#### Limitations
|
||||
|
||||
It might take up to 2 minutes for the newly upgraded member to catch up with the existing cluster when the total data size is larger than 50MB. Check the size of a recent snapshot to estimate the total data size. In other words, it is safest to wait for 2 minutes between upgrading each member.
|
||||
It might take up to 2 minutes for the newly upgraded member to catch up with the existing cluster when the total data size is larger than 50MB. Check the size of a recent snapshot to estimate the total data size. In other words, it is safest to wait for 2 minutes between upgrading each member.
|
||||
|
||||
For a much larger total data size, 100MB or more , this one-time process might take even more time. Administrators of very large etcd clusters of this magnitude can feel free to contact the [etcd team][etcd-contact] before upgrading, and we’ll be happy to provide advice on the procedure.
|
||||
|
||||
@ -36,13 +36,13 @@ If all members have been upgraded to v3.0, the cluster will be upgraded to v3.0,
|
||||
|
||||
Please [backup the data directory](../v2/admin_guide.md#backing-up-the-datastore) of all etcd members to make downgrading the cluster possible even after it has been completely upgraded.
|
||||
|
||||
### Upgrade procedure
|
||||
### Upgrade Procedure
|
||||
|
||||
This example details the upgrade of a three-member v2.3 ectd cluster running on a local machine.
|
||||
This example details the upgrade of a three-member v2.3 ectd cluster running on a local machine.
|
||||
|
||||
#### 1. Check upgrade requirements.
|
||||
|
||||
Is the cluster healthy and running v.2.3.x?
|
||||
Is the the cluster healthy and running v.2.3.x?
|
||||
|
||||
```
|
||||
$ etcdctl cluster-health
|
||||
@ -64,7 +64,7 @@ When each etcd process is stopped, expected errors will be logged by other clust
|
||||
2016-06-27 15:21:48.624175 I | rafthttp: the connection with 8211f1d0f64f3269 became inactive
|
||||
```
|
||||
|
||||
It’s a good idea at this point to [backup the etcd data directory](../v2/admin_guide.md#backing-up-the-datastore) to provide a downgrade path should any problems occur:
|
||||
It’s a good idea at this point to [backup the etcd data directory](../v2/admin_guide.md#backing-up-the-datastore) to provide a downgrade path should any problems occur:
|
||||
|
||||
```
|
||||
$ etcdctl backup \
|
||||
@ -102,7 +102,7 @@ Upgraded members will log warnings like the following until the entire cluster i
|
||||
|
||||
#### 5. Finish
|
||||
|
||||
When all members are upgraded, the cluster will report upgrading to 3.0 successfully:
|
||||
When all members are upgraded, the cluster will report upgrading to 3.0 successfully:
|
||||
|
||||
```
|
||||
2016-06-27 15:22:19.873751 N | membership: updated the cluster version from 2.3 to 3.0
|
||||
|
@ -1,123 +0,0 @@
|
||||
## Upgrade etcd from 3.0 to 3.1
|
||||
|
||||
In the general case, upgrading from etcd 3.0 to 3.1 can be a zero-downtime, rolling upgrade:
|
||||
- one by one, stop the etcd v3.0 processes and replace them with etcd v3.1 processes
|
||||
- after running all v3.1 processes, new features in v3.1 are available to the cluster
|
||||
|
||||
Before [starting an upgrade](#upgrade-procedure), read through the rest of this guide to prepare.
|
||||
|
||||
### Upgrade checklists
|
||||
|
||||
#### Upgrade requirements
|
||||
|
||||
To upgrade an existing etcd deployment to 3.1, the running cluster must be 3.0 or greater. If it's before 3.0, please upgrade to [3.0](https://github.com/coreos/etcd/releases/tag/v3.0.16) before upgrading to 3.1.
|
||||
|
||||
Also, to ensure a smooth rolling upgrade, the running cluster must be healthy. Check the health of the cluster by using the `etcdctl endpoint health` command before proceeding.
|
||||
|
||||
#### Preparation
|
||||
|
||||
Before upgrading etcd, always test the services relying on etcd in a staging environment before deploying the upgrade to the production environment.
|
||||
|
||||
Before beginning, [backup the etcd data](../op-guide/maintenance.md#snapshot-backup). Should something go wrong with the upgrade, it is possible to use this backup to [downgrade](#downgrade) back to existing etcd version. Please note that the `snapshot` command only backs up the v3 data. For v2 data, see [backing up v2 datastore](../v2/admin_guide.md#backing-up-the-datastore).
|
||||
|
||||
#### Mixed versions
|
||||
|
||||
While upgrading, an etcd cluster supports mixed versions of etcd members, and operates with the protocol of the lowest common version. The cluster is only considered upgraded once all of its members are upgraded to version 3.1. Internally, etcd members negotiate with each other to determine the overall cluster version, which controls the reported version and the supported features.
|
||||
|
||||
#### Limitations
|
||||
|
||||
Note: If the cluster only has v3 data and no v2 data, it is not subject to this limitation.
|
||||
|
||||
If the cluster is serving a v2 data set larger than 50MB, each newly upgraded member may take up to two minutes to catch up with the existing cluster. Check the size of a recent snapshot to estimate the total data size. In other words, it is safest to wait for 2 minutes between upgrading each member.
|
||||
|
||||
For a much larger total data size, 100MB or more , this one-time process might take even more time. Administrators of very large etcd clusters of this magnitude can feel free to contact the [etcd team][etcd-contact] before upgrading, and we'll be happy to provide advice on the procedure.
|
||||
|
||||
#### Downgrade
|
||||
|
||||
If all members have been upgraded to v3.1, the cluster will be upgraded to v3.1, and downgrade from this completed state is **not possible**. If any single member is still v3.0, however, the cluster and its operations remains "v3.0", and it is possible from this mixed cluster state to return to using a v3.0 etcd binary on all members.
|
||||
|
||||
Please [backup the data directory](../op-guide/maintenance.md#snapshot-backup) of all etcd members to make downgrading the cluster possible even after it has been completely upgraded.
|
||||
|
||||
### Upgrade procedure
|
||||
|
||||
This example shows how to upgrade a 3-member v3.0 ectd cluster running on a local machine.
|
||||
|
||||
#### 1. Check upgrade requirements
|
||||
|
||||
Is the cluster healthy and running v3.0.x?
|
||||
|
||||
```
|
||||
$ ETCDCTL_API=3 etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
|
||||
localhost:2379 is healthy: successfully committed proposal: took = 6.600684ms
|
||||
localhost:22379 is healthy: successfully committed proposal: took = 8.540064ms
|
||||
localhost:32379 is healthy: successfully committed proposal: took = 8.763432ms
|
||||
|
||||
$ curl http://localhost:2379/version
|
||||
{"etcdserver":"3.0.16","etcdcluster":"3.0.0"}
|
||||
```
|
||||
|
||||
#### 2. Stop the existing etcd process
|
||||
|
||||
When each etcd process is stopped, expected errors will be logged by other cluster members. This is normal since a cluster member connection has been (temporarily) broken:
|
||||
|
||||
```
|
||||
2017-01-17 09:34:18.352662 I | raft: raft.node: 1640829d9eea5cfb elected leader 1640829d9eea5cfb at term 5
|
||||
2017-01-17 09:34:18.359630 W | etcdserver: failed to reach the peerURL(http://localhost:2380) of member fd32987dcd0511e0 (Get http://localhost:2380/version: dial tcp 127.0.0.1:2380: getsockopt: connection refused)
|
||||
2017-01-17 09:34:18.359679 W | etcdserver: cannot get the version of member fd32987dcd0511e0 (Get http://localhost:2380/version: dial tcp 127.0.0.1:2380: getsockopt: connection refused)
|
||||
2017-01-17 09:34:18.548116 W | rafthttp: lost the TCP streaming connection with peer fd32987dcd0511e0 (stream Message writer)
|
||||
2017-01-17 09:34:19.147816 W | rafthttp: lost the TCP streaming connection with peer fd32987dcd0511e0 (stream MsgApp v2 writer)
|
||||
2017-01-17 09:34:34.364907 W | etcdserver: failed to reach the peerURL(http://localhost:2380) of member fd32987dcd0511e0 (Get http://localhost:2380/version: dial tcp 127.0.0.1:2380: getsockopt: connection refused)
|
||||
```
|
||||
|
||||
It's a good idea at this point to [backup the etcd data](../op-guide/maintenance.md#snapshot-backup) to provide a downgrade path should any problems occur:
|
||||
|
||||
```
|
||||
$ etcdctl snapshot save backup.db
|
||||
```
|
||||
|
||||
#### 3. Drop-in etcd v3.1 binary and start the new etcd process
|
||||
|
||||
The new v3.1 etcd will publish its information to the cluster:
|
||||
|
||||
```
|
||||
2017-01-17 09:36:00.996590 I | etcdserver: published {Name:my-etcd-1 ClientURLs:[http://localhost:2379]} to cluster 46bc3ce73049e678
|
||||
```
|
||||
|
||||
Verify that each member, and then the entire cluster, becomes healthy with the new v3.1 etcd binary:
|
||||
|
||||
```
|
||||
$ ETCDCTL_API=3 /etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
|
||||
localhost:22379 is healthy: successfully committed proposal: took = 5.540129ms
|
||||
localhost:32379 is healthy: successfully committed proposal: took = 7.321671ms
|
||||
localhost:2379 is healthy: successfully committed proposal: took = 10.629901ms
|
||||
```
|
||||
|
||||
Upgraded members will log warnings like the following until the entire cluster is upgraded. This is expected and will cease after all etcd cluster members are upgraded to v3.1:
|
||||
|
||||
```
|
||||
2017-01-17 09:36:38.406268 W | etcdserver: the local etcd version 3.0.16 is not up-to-date
|
||||
2017-01-17 09:36:38.406295 W | etcdserver: member fd32987dcd0511e0 has a higher version 3.1.0
|
||||
2017-01-17 09:36:42.407695 W | etcdserver: the local etcd version 3.0.16 is not up-to-date
|
||||
2017-01-17 09:36:42.407730 W | etcdserver: member fd32987dcd0511e0 has a higher version 3.1.0
|
||||
```
|
||||
|
||||
#### 4. Repeat step 2 to step 3 for all other members
|
||||
|
||||
#### 5. Finish
|
||||
|
||||
When all members are upgraded, the cluster will report upgrading to 3.1 successfully:
|
||||
|
||||
```
|
||||
2017-01-17 09:37:03.100015 I | etcdserver: updating the cluster version from 3.0 to 3.1
|
||||
2017-01-17 09:37:03.104263 N | etcdserver/membership: updated the cluster version from 3.0 to 3.1
|
||||
2017-01-17 09:37:03.104374 I | etcdserver/api: enabled capabilities for version 3.1
|
||||
```
|
||||
|
||||
```
|
||||
$ ETCDCTL_API=3 /etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
|
||||
localhost:2379 is healthy: successfully committed proposal: took = 2.312897ms
|
||||
localhost:22379 is healthy: successfully committed proposal: took = 2.553476ms
|
||||
localhost:32379 is healthy: successfully committed proposal: took = 2.516902ms
|
||||
```
|
||||
|
||||
[etcd-contact]: https://groups.google.com/forum/#!forum/etcd-dev
|
@ -216,7 +216,7 @@ To recover from such scenarios, etcd provides functionality to backup and restor
|
||||
|
||||
#### Backing up the datastore
|
||||
|
||||
**Note:** Windows users must stop etcd before running the backup command.
|
||||
**NB:** Windows users must stop etcd before running the backup command.
|
||||
|
||||
The first step of the recovery is to backup the data directory and wal directory, if stored separately, on a functioning etcd node. To do this, use the `etcdctl backup` command, passing in the original data (and wal) directory used by etcd. For example:
|
||||
|
||||
@ -262,9 +262,7 @@ Once you have verified that etcd has started successfully, shut it down and move
|
||||
|
||||
Now that the node is running successfully, [change its advertised peer URLs][update-a-member], as the `--force-new-cluster` option has set the peer URL to the default listening on localhost.
|
||||
|
||||
You can then add more nodes to the cluster and restore resiliency. See the [add a new member][add-a-member] guide for more details.
|
||||
|
||||
**Note:** If you are trying to restore your cluster using old failed etcd nodes, please make sure you have stopped old etcd instances and removed their old data directories specified by the data-dir configuration parameter.
|
||||
You can then add more nodes to the cluster and restore resiliency. See the [add a new member][add-a-member] guide for more details. **NB:** If you are trying to restore your cluster using old failed etcd nodes, please make sure you have stopped old etcd instances and removed their old data directories specified by the data-dir configuration parameter.
|
||||
|
||||
### Client Request Timeout
|
||||
|
||||
|
@ -559,25 +559,6 @@ Let's create a key-value pair first: `foo=one`.
|
||||
curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=one
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action":"set",
|
||||
"node":{
|
||||
"key":"/foo",
|
||||
"value":"one",
|
||||
"modifiedIndex":4,
|
||||
"createdIndex":4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Specifying `noValueOnSuccess` option skips returning the node as value.
|
||||
|
||||
```sh
|
||||
curl http://127.0.0.1:2379/v2/keys/foo?noValueOnSuccess=true -XPUT -d value=one
|
||||
# {"action":"set"}
|
||||
```
|
||||
|
||||
Now let's try some invalid `CompareAndSwap` commands.
|
||||
|
||||
Trying to set this existing key with `prevExist=false` fails as expected:
|
||||
|
@ -266,7 +266,7 @@ Follow the instructions when using these flags.
|
||||
## Profiling flags
|
||||
|
||||
### --enable-pprof
|
||||
+ Enable runtime profiling data via HTTP server. Address is at client URL + "/debug/pprof/"
|
||||
+ Enable runtime profiling data via HTTP server. Address is at client URL + "/debug/pprof"
|
||||
+ default: false
|
||||
|
||||
[build-cluster]: clustering.md#static
|
||||
|
@ -48,7 +48,7 @@ All releases version numbers follow the format of [semantic versioning 2.0.0](ht
|
||||
|
||||
## Build Release Binaries and Images
|
||||
|
||||
- Ensure `acbuild` is available.
|
||||
- Ensure `actool` is available, or installing it through `go get github.com/appc/spec/actool`.
|
||||
- Ensure `docker` is available.
|
||||
|
||||
Run release script in root directory:
|
||||
|
@ -105,7 +105,7 @@ ETCD_INITIAL_CLUSTER_STATE=existing
|
||||
|
||||
### Stop the proxy process
|
||||
|
||||
Stop the existing proxy so we can wipe its state on disk and reload it with the new configuration:
|
||||
Stop the existing proxy so we can wipe it's state on disk and reload it with the new configuration:
|
||||
|
||||
``` bash
|
||||
px aux | grep etcd
|
||||
@ -149,5 +149,5 @@ If an error occurs, check the [add member troubleshooting doc][runtime-configura
|
||||
|
||||
[discovery-service]: clustering.md#discovery
|
||||
[goreman]: https://github.com/mattn/goreman
|
||||
[procfile]: https://github.com/coreos/etcd/blob/master/Procfile
|
||||
[procfile]: /Procfile
|
||||
[runtime-configuration]: runtime-configuration.md#error-cases-when-adding-members
|
||||
|
81
NEWS
81
NEWS
@ -1,81 +0,0 @@
|
||||
etcd v3.1.0 (2017-01-20)
|
||||
- faster linearizable reads (implements Raft read-index)
|
||||
- automatic leadership transfer when leader steps down
|
||||
- etcd uses default route IP if advertise URL is not given
|
||||
- cluster rejects removing members if quorum will be lost
|
||||
- SRV records (e.g., infra1.example.com) must match the discovery domain
|
||||
(i.e., example.com) if no custom certificate authority is given
|
||||
- TLSConfig ServerName is ignored with user-provided certificates
|
||||
for backwards compatibility; to be deprecated in 3.2
|
||||
- discovery now has upper limit for waiting on retries
|
||||
- etcd flags
|
||||
- --strict-reconfig-check flag is set by default
|
||||
- add --log-output flag
|
||||
- add --metrics flag
|
||||
- v3 authentication API is now stable
|
||||
- v3 client
|
||||
- add SetEndpoints method; update endpoints at runtime
|
||||
- add Sync method; auto-update endpoints at runtime
|
||||
- add Lease TimeToLive API; fetch lease information
|
||||
- replace Config.Logger field with global logger
|
||||
- Get API responses are sorted in ascending order by default
|
||||
- v3 etcdctl
|
||||
- add lease timetolive command
|
||||
- add --print-value-only flag to get command
|
||||
- add --dest-prefix flag to make-mirror command
|
||||
- command get responses are sorted in ascending order by default
|
||||
- recipes now conform to sessions defined in clientv3/concurrency
|
||||
- ACI has symlinks to /usr/local/bin/etcd*
|
||||
- warn on binding listeners through domain names; to be deprecated in 3.2
|
||||
- experimental gRPC proxy feature
|
||||
|
||||
etcd v3.0.16 (2017-01-13)
|
||||
|
||||
etcd v3.0.15 (2016-11-11)
|
||||
- fix cancel watch request with wrong range end
|
||||
|
||||
etcd v3.0.14 (2016-11-04)
|
||||
- v3 etcdctl migrate command now supports --no-ttl flag to discard keys on transform
|
||||
|
||||
etcd v3.0.13 (2016-10-24)
|
||||
|
||||
etcd v3.0.12 (2016-10-07)
|
||||
|
||||
etcd v3.0.11 (2016-10-07)
|
||||
- server returns previous key-value (optional)
|
||||
- clientv3 WithPrevKV option
|
||||
- v3 etcdctl put,watch,del --prev-kv flag
|
||||
|
||||
etcd v3.0.10 (2016-09-23)
|
||||
|
||||
etcd v3.0.9 (2016-09-15)
|
||||
- warn on domain names on listen URLs (v3.2 will reject domain names)
|
||||
|
||||
etcd v3.0.8 (2016-09-09)
|
||||
- allow only IP addresses in listen URLs (domain names are rejected)
|
||||
|
||||
etcd v3.0.7 (2016-08-31)
|
||||
- SRV records only allow A records (RFC 2052)
|
||||
|
||||
etcd v3.0.6 (2016-08-19)
|
||||
|
||||
etcd v3.0.5 (2016-08-19)
|
||||
- SRV records (e.g., infra1.example.com) must match the discovery domain
|
||||
(i.e., example.com) if no custom certificate authority is given
|
||||
|
||||
etcd v3.0.4 (2016-07-27)
|
||||
- v2 auth can now use common name from TLS certificate when --client-cert-auth is enabled
|
||||
- v2 etcdctl ls command now supports --output=json
|
||||
- Add /var/lib/etcd directory to etcd official Docker image
|
||||
|
||||
etcd v3.0.3 (2016-07-15)
|
||||
- Revert Dockerfile to use CMD, instead of ENTRYPOINT, to support etcdctl run
|
||||
- Docker commands for v3.0.2 won't work without specifying executable binary paths
|
||||
- v3 etcdctl default endpoints are now 127.0.0.1:2379
|
||||
|
||||
etcd v3.0.2 (2016-07-08)
|
||||
- Dockerfile uses ENTRYPOINT, instead of CMD, to run etcd without binary path specified
|
||||
|
||||
etcd v3.0.1 (2016-07-01)
|
||||
|
||||
etcd v3.0.0 (2016-06-30)
|
13
README.md
13
README.md
@ -37,17 +37,15 @@ See [etcdctl][etcdctl] for a simple command line client.
|
||||
|
||||
### Getting etcd
|
||||
|
||||
The easiest way to get etcd is to use one of the pre-built release binaries which are available for OSX, Linux, Windows, [rkt][rkt], and Docker. Instructions for using these binaries are on the [GitHub releases page][github-release].
|
||||
The easiest way to get etcd is to use one of the pre-built release binaries which are available for OSX, Linux, Windows, AppC (ACI), and Docker. Instructions for using these binaries are on the [GitHub releases page][github-release].
|
||||
|
||||
For those wanting to try the very latest version, you can [build the latest version of etcd][dl-build] from the `master` branch.
|
||||
You will first need [*Go*](https://golang.org/) installed on your machine (version 1.7+ is required).
|
||||
For those wanting to try the very latest version, you can build the latest version of etcd from the `master` branch.
|
||||
You will first need [*Go*](https://golang.org/) installed on your machine (version 1.6+ is required).
|
||||
All development occurs on `master`, including new features and bug fixes.
|
||||
Bug fixes are first targeted at `master` and subsequently ported to release branches, as described in the [branch management][branch-management] guide.
|
||||
|
||||
[rkt]: https://github.com/coreos/rkt/releases/
|
||||
[github-release]: https://github.com/coreos/etcd/releases/
|
||||
[branch-management]: ./Documentation/branch_management.md
|
||||
[dl-build]: ./Documentation/dl_build.md#build-the-latest-version
|
||||
|
||||
### Running etcd
|
||||
|
||||
@ -94,10 +92,6 @@ This will bring up 3 etcd members `infra1`, `infra2` and `infra3` and etcd proxy
|
||||
|
||||
Every cluster member and proxy accepts key value reads and key value writes.
|
||||
|
||||
### Running etcd on Kubernetes
|
||||
|
||||
If you want to run etcd cluster on Kubernetes, try [etcd operator](https://github.com/coreos/etcd-operator).
|
||||
|
||||
### Next steps
|
||||
|
||||
Now it's time to dig into the full etcd API and other guides.
|
||||
@ -137,4 +131,3 @@ See [reporting bugs](Documentation/reporting_bugs.md) for details about reportin
|
||||
|
||||
etcd is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
|
||||
|
32
ROADMAP.md
32
ROADMAP.md
@ -6,18 +6,26 @@ This document defines a high level roadmap for etcd development.
|
||||
|
||||
The dates below should not be considered authoritative, but rather indicative of the projected timeline of the project. The [milestones defined in GitHub](https://github.com/coreos/etcd/milestones) represent the most up-to-date and issue-for-issue plans.
|
||||
|
||||
etcd 3.1 is our current stable branch. The roadmap below outlines new features that will be added to etcd, and while subject to change, define what future stable will look like.
|
||||
etcd 2.3 is our current stable branch. The roadmap below outlines new features that will be added to etcd, and while subject to change, define what future stable will look like.
|
||||
|
||||
### etcd 3.2 (2017-May)
|
||||
- Stable scalable proxy
|
||||
- Proxy-as-client interface passthrough
|
||||
- Lock service
|
||||
- Namespacing proxy
|
||||
- TLS Command Name and JWT token based authentication
|
||||
- Read-modify-write V3 Put
|
||||
- Improved watch performance
|
||||
- Support non-blocking concurrent read
|
||||
### etcd 3.0 (April)
|
||||
- v3 API ([see also the issue tag](https://github.com/coreos/etcd/issues?utf8=%E2%9C%93&q=label%3Aarea/v3api))
|
||||
- Leases
|
||||
- Binary protocol
|
||||
- Support a large number of watchers
|
||||
- Failure guarantees documented
|
||||
- Simple v3 client (golang)
|
||||
- v3 API
|
||||
- Locking
|
||||
- Better disk backend
|
||||
- Improved write throughput
|
||||
- Support larger datasets and histories
|
||||
- Simpler disaster recovery UX
|
||||
- Integrated with Kubernetes
|
||||
- Mirroring
|
||||
|
||||
### etcd 3.3 (?)
|
||||
- TBD
|
||||
### etcd 3.1 (July)
|
||||
- API bindings for other languages
|
||||
|
||||
### etcd 3.+ (future)
|
||||
- Horizontally scalable proxy layer
|
||||
|
@ -32,9 +32,7 @@ var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
const _ = proto.ProtoPackageIsVersion1
|
||||
|
||||
type Permission_Type int32
|
||||
|
||||
@ -101,113 +99,113 @@ func init() {
|
||||
proto.RegisterType((*Role)(nil), "authpb.Role")
|
||||
proto.RegisterEnum("authpb.Permission_Type", Permission_Type_name, Permission_Type_value)
|
||||
}
|
||||
func (m *User) Marshal() (dAtA []byte, err error) {
|
||||
func (m *User) Marshal() (data []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalTo(dAtA)
|
||||
data = make([]byte, size)
|
||||
n, err := m.MarshalTo(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
return data[:n], nil
|
||||
}
|
||||
|
||||
func (m *User) MarshalTo(dAtA []byte) (int, error) {
|
||||
func (m *User) MarshalTo(data []byte) (int, error) {
|
||||
var i int
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.Name) > 0 {
|
||||
dAtA[i] = 0xa
|
||||
data[i] = 0xa
|
||||
i++
|
||||
i = encodeVarintAuth(dAtA, i, uint64(len(m.Name)))
|
||||
i += copy(dAtA[i:], m.Name)
|
||||
i = encodeVarintAuth(data, i, uint64(len(m.Name)))
|
||||
i += copy(data[i:], m.Name)
|
||||
}
|
||||
if len(m.Password) > 0 {
|
||||
dAtA[i] = 0x12
|
||||
data[i] = 0x12
|
||||
i++
|
||||
i = encodeVarintAuth(dAtA, i, uint64(len(m.Password)))
|
||||
i += copy(dAtA[i:], m.Password)
|
||||
i = encodeVarintAuth(data, i, uint64(len(m.Password)))
|
||||
i += copy(data[i:], m.Password)
|
||||
}
|
||||
if len(m.Roles) > 0 {
|
||||
for _, s := range m.Roles {
|
||||
dAtA[i] = 0x1a
|
||||
data[i] = 0x1a
|
||||
i++
|
||||
l = len(s)
|
||||
for l >= 1<<7 {
|
||||
dAtA[i] = uint8(uint64(l)&0x7f | 0x80)
|
||||
data[i] = uint8(uint64(l)&0x7f | 0x80)
|
||||
l >>= 7
|
||||
i++
|
||||
}
|
||||
dAtA[i] = uint8(l)
|
||||
data[i] = uint8(l)
|
||||
i++
|
||||
i += copy(dAtA[i:], s)
|
||||
i += copy(data[i:], s)
|
||||
}
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (m *Permission) Marshal() (dAtA []byte, err error) {
|
||||
func (m *Permission) Marshal() (data []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalTo(dAtA)
|
||||
data = make([]byte, size)
|
||||
n, err := m.MarshalTo(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
return data[:n], nil
|
||||
}
|
||||
|
||||
func (m *Permission) MarshalTo(dAtA []byte) (int, error) {
|
||||
func (m *Permission) MarshalTo(data []byte) (int, error) {
|
||||
var i int
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if m.PermType != 0 {
|
||||
dAtA[i] = 0x8
|
||||
data[i] = 0x8
|
||||
i++
|
||||
i = encodeVarintAuth(dAtA, i, uint64(m.PermType))
|
||||
i = encodeVarintAuth(data, i, uint64(m.PermType))
|
||||
}
|
||||
if len(m.Key) > 0 {
|
||||
dAtA[i] = 0x12
|
||||
data[i] = 0x12
|
||||
i++
|
||||
i = encodeVarintAuth(dAtA, i, uint64(len(m.Key)))
|
||||
i += copy(dAtA[i:], m.Key)
|
||||
i = encodeVarintAuth(data, i, uint64(len(m.Key)))
|
||||
i += copy(data[i:], m.Key)
|
||||
}
|
||||
if len(m.RangeEnd) > 0 {
|
||||
dAtA[i] = 0x1a
|
||||
data[i] = 0x1a
|
||||
i++
|
||||
i = encodeVarintAuth(dAtA, i, uint64(len(m.RangeEnd)))
|
||||
i += copy(dAtA[i:], m.RangeEnd)
|
||||
i = encodeVarintAuth(data, i, uint64(len(m.RangeEnd)))
|
||||
i += copy(data[i:], m.RangeEnd)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (m *Role) Marshal() (dAtA []byte, err error) {
|
||||
func (m *Role) Marshal() (data []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalTo(dAtA)
|
||||
data = make([]byte, size)
|
||||
n, err := m.MarshalTo(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
return data[:n], nil
|
||||
}
|
||||
|
||||
func (m *Role) MarshalTo(dAtA []byte) (int, error) {
|
||||
func (m *Role) MarshalTo(data []byte) (int, error) {
|
||||
var i int
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.Name) > 0 {
|
||||
dAtA[i] = 0xa
|
||||
data[i] = 0xa
|
||||
i++
|
||||
i = encodeVarintAuth(dAtA, i, uint64(len(m.Name)))
|
||||
i += copy(dAtA[i:], m.Name)
|
||||
i = encodeVarintAuth(data, i, uint64(len(m.Name)))
|
||||
i += copy(data[i:], m.Name)
|
||||
}
|
||||
if len(m.KeyPermission) > 0 {
|
||||
for _, msg := range m.KeyPermission {
|
||||
dAtA[i] = 0x12
|
||||
data[i] = 0x12
|
||||
i++
|
||||
i = encodeVarintAuth(dAtA, i, uint64(msg.Size()))
|
||||
n, err := msg.MarshalTo(dAtA[i:])
|
||||
i = encodeVarintAuth(data, i, uint64(msg.Size()))
|
||||
n, err := msg.MarshalTo(data[i:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -217,31 +215,31 @@ func (m *Role) MarshalTo(dAtA []byte) (int, error) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func encodeFixed64Auth(dAtA []byte, offset int, v uint64) int {
|
||||
dAtA[offset] = uint8(v)
|
||||
dAtA[offset+1] = uint8(v >> 8)
|
||||
dAtA[offset+2] = uint8(v >> 16)
|
||||
dAtA[offset+3] = uint8(v >> 24)
|
||||
dAtA[offset+4] = uint8(v >> 32)
|
||||
dAtA[offset+5] = uint8(v >> 40)
|
||||
dAtA[offset+6] = uint8(v >> 48)
|
||||
dAtA[offset+7] = uint8(v >> 56)
|
||||
func encodeFixed64Auth(data []byte, offset int, v uint64) int {
|
||||
data[offset] = uint8(v)
|
||||
data[offset+1] = uint8(v >> 8)
|
||||
data[offset+2] = uint8(v >> 16)
|
||||
data[offset+3] = uint8(v >> 24)
|
||||
data[offset+4] = uint8(v >> 32)
|
||||
data[offset+5] = uint8(v >> 40)
|
||||
data[offset+6] = uint8(v >> 48)
|
||||
data[offset+7] = uint8(v >> 56)
|
||||
return offset + 8
|
||||
}
|
||||
func encodeFixed32Auth(dAtA []byte, offset int, v uint32) int {
|
||||
dAtA[offset] = uint8(v)
|
||||
dAtA[offset+1] = uint8(v >> 8)
|
||||
dAtA[offset+2] = uint8(v >> 16)
|
||||
dAtA[offset+3] = uint8(v >> 24)
|
||||
func encodeFixed32Auth(data []byte, offset int, v uint32) int {
|
||||
data[offset] = uint8(v)
|
||||
data[offset+1] = uint8(v >> 8)
|
||||
data[offset+2] = uint8(v >> 16)
|
||||
data[offset+3] = uint8(v >> 24)
|
||||
return offset + 4
|
||||
}
|
||||
func encodeVarintAuth(dAtA []byte, offset int, v uint64) int {
|
||||
func encodeVarintAuth(data []byte, offset int, v uint64) int {
|
||||
for v >= 1<<7 {
|
||||
dAtA[offset] = uint8(v&0x7f | 0x80)
|
||||
data[offset] = uint8(v&0x7f | 0x80)
|
||||
v >>= 7
|
||||
offset++
|
||||
}
|
||||
dAtA[offset] = uint8(v)
|
||||
data[offset] = uint8(v)
|
||||
return offset + 1
|
||||
}
|
||||
func (m *User) Size() (n int) {
|
||||
@ -310,8 +308,8 @@ func sovAuth(x uint64) (n int) {
|
||||
func sozAuth(x uint64) (n int) {
|
||||
return sovAuth(uint64((x << 1) ^ uint64((int64(x) >> 63))))
|
||||
}
|
||||
func (m *User) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
func (m *User) Unmarshal(data []byte) error {
|
||||
l := len(data)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
@ -323,7 +321,7 @@ func (m *User) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -351,7 +349,7 @@ func (m *User) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
byteLen |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -365,7 +363,7 @@ func (m *User) Unmarshal(dAtA []byte) error {
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Name = append(m.Name[:0], dAtA[iNdEx:postIndex]...)
|
||||
m.Name = append(m.Name[:0], data[iNdEx:postIndex]...)
|
||||
if m.Name == nil {
|
||||
m.Name = []byte{}
|
||||
}
|
||||
@ -382,7 +380,7 @@ func (m *User) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
byteLen |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -396,7 +394,7 @@ func (m *User) Unmarshal(dAtA []byte) error {
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Password = append(m.Password[:0], dAtA[iNdEx:postIndex]...)
|
||||
m.Password = append(m.Password[:0], data[iNdEx:postIndex]...)
|
||||
if m.Password == nil {
|
||||
m.Password = []byte{}
|
||||
}
|
||||
@ -413,7 +411,7 @@ func (m *User) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -428,11 +426,11 @@ func (m *User) Unmarshal(dAtA []byte) error {
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Roles = append(m.Roles, string(dAtA[iNdEx:postIndex]))
|
||||
m.Roles = append(m.Roles, string(data[iNdEx:postIndex]))
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipAuth(dAtA[iNdEx:])
|
||||
skippy, err := skipAuth(data[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -451,8 +449,8 @@ func (m *User) Unmarshal(dAtA []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *Permission) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
func (m *Permission) Unmarshal(data []byte) error {
|
||||
l := len(data)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
@ -464,7 +462,7 @@ func (m *Permission) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -492,7 +490,7 @@ func (m *Permission) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
m.PermType |= (Permission_Type(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -511,7 +509,7 @@ func (m *Permission) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
byteLen |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -525,7 +523,7 @@ func (m *Permission) Unmarshal(dAtA []byte) error {
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Key = append(m.Key[:0], dAtA[iNdEx:postIndex]...)
|
||||
m.Key = append(m.Key[:0], data[iNdEx:postIndex]...)
|
||||
if m.Key == nil {
|
||||
m.Key = []byte{}
|
||||
}
|
||||
@ -542,7 +540,7 @@ func (m *Permission) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
byteLen |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -556,14 +554,14 @@ func (m *Permission) Unmarshal(dAtA []byte) error {
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.RangeEnd = append(m.RangeEnd[:0], dAtA[iNdEx:postIndex]...)
|
||||
m.RangeEnd = append(m.RangeEnd[:0], data[iNdEx:postIndex]...)
|
||||
if m.RangeEnd == nil {
|
||||
m.RangeEnd = []byte{}
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipAuth(dAtA[iNdEx:])
|
||||
skippy, err := skipAuth(data[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -582,8 +580,8 @@ func (m *Permission) Unmarshal(dAtA []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *Role) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
func (m *Role) Unmarshal(data []byte) error {
|
||||
l := len(data)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
@ -595,7 +593,7 @@ func (m *Role) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -623,7 +621,7 @@ func (m *Role) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
byteLen |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -637,7 +635,7 @@ func (m *Role) Unmarshal(dAtA []byte) error {
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Name = append(m.Name[:0], dAtA[iNdEx:postIndex]...)
|
||||
m.Name = append(m.Name[:0], data[iNdEx:postIndex]...)
|
||||
if m.Name == nil {
|
||||
m.Name = []byte{}
|
||||
}
|
||||
@ -654,7 +652,7 @@ func (m *Role) Unmarshal(dAtA []byte) error {
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
msglen |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -669,13 +667,13 @@ func (m *Role) Unmarshal(dAtA []byte) error {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.KeyPermission = append(m.KeyPermission, &Permission{})
|
||||
if err := m.KeyPermission[len(m.KeyPermission)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
if err := m.KeyPermission[len(m.KeyPermission)-1].Unmarshal(data[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipAuth(dAtA[iNdEx:])
|
||||
skippy, err := skipAuth(data[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -694,8 +692,8 @@ func (m *Role) Unmarshal(dAtA []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func skipAuth(dAtA []byte) (n int, err error) {
|
||||
l := len(dAtA)
|
||||
func skipAuth(data []byte) (n int, err error) {
|
||||
l := len(data)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
var wire uint64
|
||||
@ -706,7 +704,7 @@ func skipAuth(dAtA []byte) (n int, err error) {
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -724,7 +722,7 @@ func skipAuth(dAtA []byte) (n int, err error) {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
iNdEx++
|
||||
if dAtA[iNdEx-1] < 0x80 {
|
||||
if data[iNdEx-1] < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -741,7 +739,7 @@ func skipAuth(dAtA []byte) (n int, err error) {
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
length |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -764,7 +762,7 @@ func skipAuth(dAtA []byte) (n int, err error) {
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
innerWire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
@ -775,7 +773,7 @@ func skipAuth(dAtA []byte) (n int, err error) {
|
||||
if innerWireType == 4 {
|
||||
break
|
||||
}
|
||||
next, err := skipAuth(dAtA[start:])
|
||||
next, err := skipAuth(data[start:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -799,8 +797,6 @@ var (
|
||||
ErrIntOverflowAuth = fmt.Errorf("proto: integer overflow")
|
||||
)
|
||||
|
||||
func init() { proto.RegisterFile("auth.proto", fileDescriptorAuth) }
|
||||
|
||||
var fileDescriptorAuth = []byte{
|
||||
// 288 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0xc1, 0x4a, 0xc3, 0x30,
|
||||
|
@ -49,37 +49,46 @@ func isRangeEqual(a, b *rangePerm) bool {
|
||||
|
||||
// removeSubsetRangePerms removes any rangePerms that are subsets of other rangePerms.
|
||||
// If there are equal ranges, removeSubsetRangePerms only keeps one of them.
|
||||
// It returns a sorted rangePerm slice.
|
||||
func removeSubsetRangePerms(perms []*rangePerm) (newp []*rangePerm) {
|
||||
sort.Sort(RangePermSliceByBegin(perms))
|
||||
var prev *rangePerm
|
||||
func removeSubsetRangePerms(perms []*rangePerm) []*rangePerm {
|
||||
// TODO(mitake): currently it is O(n^2), we need a better algorithm
|
||||
newp := make([]*rangePerm, 0)
|
||||
|
||||
for i := range perms {
|
||||
if i == 0 {
|
||||
prev = perms[i]
|
||||
newp = append(newp, perms[i])
|
||||
skip := false
|
||||
|
||||
for j := range perms {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
|
||||
if isRangeEqual(perms[i], perms[j]) {
|
||||
// if ranges are equal, we only keep the first range.
|
||||
if i > j {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
} else if isSubset(perms[i], perms[j]) {
|
||||
// if a range is a strict subset of the other one, we skip the subset.
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
if isRangeEqual(perms[i], prev) {
|
||||
continue
|
||||
}
|
||||
if isSubset(perms[i], prev) {
|
||||
continue
|
||||
}
|
||||
if isSubset(prev, perms[i]) {
|
||||
prev = perms[i]
|
||||
newp[len(newp)-1] = perms[i]
|
||||
continue
|
||||
}
|
||||
prev = perms[i]
|
||||
|
||||
newp = append(newp, perms[i])
|
||||
}
|
||||
|
||||
return newp
|
||||
}
|
||||
|
||||
// mergeRangePerms merges adjacent rangePerms.
|
||||
func mergeRangePerms(perms []*rangePerm) []*rangePerm {
|
||||
var merged []*rangePerm
|
||||
merged := make([]*rangePerm, 0)
|
||||
perms = removeSubsetRangePerms(perms)
|
||||
sort.Sort(RangePermSliceByBegin(perms))
|
||||
|
||||
i := 0
|
||||
for i < len(perms) {
|
||||
|
@ -16,8 +16,6 @@ package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -133,47 +131,3 @@ func TestGetMergedPerms(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveSubsetRangePerms(t *testing.T) {
|
||||
tests := []struct {
|
||||
perms []*rangePerm
|
||||
expect []*rangePerm
|
||||
}{
|
||||
{ // subsets converge
|
||||
[]*rangePerm{{[]byte{2}, []byte{3}}, {[]byte{2}, []byte{5}}, {[]byte{1}, []byte{4}}},
|
||||
[]*rangePerm{{[]byte{1}, []byte{4}}, {[]byte{2}, []byte{5}}},
|
||||
},
|
||||
{ // subsets converge
|
||||
[]*rangePerm{{[]byte{0}, []byte{3}}, {[]byte{0}, []byte{1}}, {[]byte{2}, []byte{4}}, {[]byte{0}, []byte{2}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{3}}, {[]byte{2}, []byte{4}}},
|
||||
},
|
||||
{ // biggest range at the end
|
||||
[]*rangePerm{{[]byte{2}, []byte{3}}, {[]byte{0}, []byte{2}}, {[]byte{1}, []byte{4}}, {[]byte{0}, []byte{5}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{5}}},
|
||||
},
|
||||
{ // biggest range at the beginning
|
||||
[]*rangePerm{{[]byte{0}, []byte{5}}, {[]byte{2}, []byte{3}}, {[]byte{0}, []byte{2}}, {[]byte{1}, []byte{4}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{5}}},
|
||||
},
|
||||
{ // no overlapping ranges
|
||||
[]*rangePerm{{[]byte{2}, []byte{3}}, {[]byte{0}, []byte{1}}, {[]byte{4}, []byte{7}}, {[]byte{8}, []byte{15}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{1}}, {[]byte{2}, []byte{3}}, {[]byte{4}, []byte{7}}, {[]byte{8}, []byte{15}}},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
rs := removeSubsetRangePerms(tt.perms)
|
||||
if !reflect.DeepEqual(rs, tt.expect) {
|
||||
t.Fatalf("#%d: unexpected rangePerms %q, got %q", i, printPerms(rs), printPerms(tt.expect))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printPerms(rs []*rangePerm) (txt string) {
|
||||
for i, p := range rs {
|
||||
if i != 0 {
|
||||
txt += ","
|
||||
}
|
||||
txt += fmt.Sprintf("%+v", *p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -20,86 +20,13 @@ package auth
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
defaultSimpleTokenLength = 16
|
||||
simpleTokenTTL = 5 * time.Minute
|
||||
simpleTokenTTLResolution = 1 * time.Second
|
||||
)
|
||||
|
||||
type simpleTokenTTLKeeper struct {
|
||||
tokens map[string]time.Time
|
||||
addSimpleTokenCh chan string
|
||||
resetSimpleTokenCh chan string
|
||||
deleteSimpleTokenCh chan string
|
||||
stopCh chan chan struct{}
|
||||
deleteTokenFunc func(string)
|
||||
}
|
||||
|
||||
func NewSimpleTokenTTLKeeper(deletefunc func(string)) *simpleTokenTTLKeeper {
|
||||
stk := &simpleTokenTTLKeeper{
|
||||
tokens: make(map[string]time.Time),
|
||||
addSimpleTokenCh: make(chan string, 1),
|
||||
resetSimpleTokenCh: make(chan string, 1),
|
||||
deleteSimpleTokenCh: make(chan string, 1),
|
||||
stopCh: make(chan chan struct{}),
|
||||
deleteTokenFunc: deletefunc,
|
||||
}
|
||||
go stk.run()
|
||||
return stk
|
||||
}
|
||||
|
||||
func (tm *simpleTokenTTLKeeper) stop() {
|
||||
waitCh := make(chan struct{})
|
||||
tm.stopCh <- waitCh
|
||||
<-waitCh
|
||||
close(tm.stopCh)
|
||||
}
|
||||
|
||||
func (tm *simpleTokenTTLKeeper) addSimpleToken(token string) {
|
||||
tm.addSimpleTokenCh <- token
|
||||
}
|
||||
|
||||
func (tm *simpleTokenTTLKeeper) resetSimpleToken(token string) {
|
||||
tm.resetSimpleTokenCh <- token
|
||||
}
|
||||
|
||||
func (tm *simpleTokenTTLKeeper) deleteSimpleToken(token string) {
|
||||
tm.deleteSimpleTokenCh <- token
|
||||
}
|
||||
func (tm *simpleTokenTTLKeeper) run() {
|
||||
tokenTicker := time.NewTicker(simpleTokenTTLResolution)
|
||||
defer tokenTicker.Stop()
|
||||
for {
|
||||
select {
|
||||
case t := <-tm.addSimpleTokenCh:
|
||||
tm.tokens[t] = time.Now().Add(simpleTokenTTL)
|
||||
case t := <-tm.resetSimpleTokenCh:
|
||||
if _, ok := tm.tokens[t]; ok {
|
||||
tm.tokens[t] = time.Now().Add(simpleTokenTTL)
|
||||
}
|
||||
case t := <-tm.deleteSimpleTokenCh:
|
||||
delete(tm.tokens, t)
|
||||
case <-tokenTicker.C:
|
||||
nowtime := time.Now()
|
||||
for t, tokenendtime := range tm.tokens {
|
||||
if nowtime.After(tokenendtime) {
|
||||
tm.deleteTokenFunc(t)
|
||||
delete(tm.tokens, t)
|
||||
}
|
||||
}
|
||||
case waitCh := <-tm.stopCh:
|
||||
tm.tokens = make(map[string]time.Time)
|
||||
waitCh <- struct{}{}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (as *authStore) GenSimpleToken() (string, error) {
|
||||
ret := make([]byte, defaultSimpleTokenLength)
|
||||
|
||||
@ -124,18 +51,5 @@ func (as *authStore) assignSimpleTokenToUser(username, token string) {
|
||||
}
|
||||
|
||||
as.simpleTokens[token] = username
|
||||
as.simpleTokenKeeper.addSimpleToken(token)
|
||||
as.simpleTokensMu.Unlock()
|
||||
}
|
||||
|
||||
func (as *authStore) invalidateUser(username string) {
|
||||
as.simpleTokensMu.Lock()
|
||||
defer as.simpleTokensMu.Unlock()
|
||||
|
||||
for token, name := range as.simpleTokens {
|
||||
if strings.Compare(name, username) == 0 {
|
||||
delete(as.simpleTokens, token)
|
||||
as.simpleTokenKeeper.deleteSimpleToken(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
327
auth/store.go
327
auth/store.go
@ -16,11 +16,9 @@ package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -30,7 +28,6 @@ import (
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -38,8 +35,6 @@ var (
|
||||
authEnabled = []byte{1}
|
||||
authDisabled = []byte{0}
|
||||
|
||||
revisionKey = []byte("authRevision")
|
||||
|
||||
authBucketName = []byte("auth")
|
||||
authUsersBucketName = []byte("authUsers")
|
||||
authRolesBucketName = []byte("authRoles")
|
||||
@ -49,7 +44,6 @@ var (
|
||||
ErrRootUserNotExist = errors.New("auth: root user does not exist")
|
||||
ErrRootRoleNotExist = errors.New("auth: root user does not have root role")
|
||||
ErrUserAlreadyExist = errors.New("auth: user already exists")
|
||||
ErrUserEmpty = errors.New("auth: user name is empty")
|
||||
ErrUserNotFound = errors.New("auth: user not found")
|
||||
ErrRoleAlreadyExist = errors.New("auth: role already exists")
|
||||
ErrRoleNotFound = errors.New("auth: role not found")
|
||||
@ -57,26 +51,13 @@ var (
|
||||
ErrPermissionDenied = errors.New("auth: permission denied")
|
||||
ErrRoleNotGranted = errors.New("auth: role is not granted to the user")
|
||||
ErrPermissionNotGranted = errors.New("auth: permission is not granted to the role")
|
||||
ErrAuthNotEnabled = errors.New("auth: authentication is not enabled")
|
||||
ErrAuthOldRevision = errors.New("auth: revision in header is old")
|
||||
ErrInvalidAuthToken = errors.New("auth: invalid auth token")
|
||||
|
||||
// BcryptCost is the algorithm cost / strength for hashing auth passwords
|
||||
BcryptCost = bcrypt.DefaultCost
|
||||
)
|
||||
|
||||
const (
|
||||
rootUser = "root"
|
||||
rootRole = "root"
|
||||
|
||||
revBytesLen = 8
|
||||
)
|
||||
|
||||
type AuthInfo struct {
|
||||
Username string
|
||||
Revision uint64
|
||||
}
|
||||
|
||||
type AuthStore interface {
|
||||
// AuthEnable turns on the authentication feature
|
||||
AuthEnable() error
|
||||
@ -129,36 +110,23 @@ type AuthStore interface {
|
||||
// RoleList gets a list of all roles
|
||||
RoleList(r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error)
|
||||
|
||||
// AuthInfoFromToken gets a username from the given Token and current revision number
|
||||
// (The revision number is used for preventing the TOCTOU problem)
|
||||
AuthInfoFromToken(token string) (*AuthInfo, bool)
|
||||
// UsernameFromToken gets a username from the given Token
|
||||
UsernameFromToken(token string) (string, bool)
|
||||
|
||||
// IsPutPermitted checks put permission of the user
|
||||
IsPutPermitted(authInfo *AuthInfo, key []byte) error
|
||||
IsPutPermitted(username string, key []byte) bool
|
||||
|
||||
// IsRangePermitted checks range permission of the user
|
||||
IsRangePermitted(authInfo *AuthInfo, key, rangeEnd []byte) error
|
||||
IsRangePermitted(username string, key, rangeEnd []byte) bool
|
||||
|
||||
// IsDeleteRangePermitted checks delete-range permission of the user
|
||||
IsDeleteRangePermitted(authInfo *AuthInfo, key, rangeEnd []byte) error
|
||||
IsDeleteRangePermitted(username string, key, rangeEnd []byte) bool
|
||||
|
||||
// IsAdminPermitted checks admin permission of the user
|
||||
IsAdminPermitted(authInfo *AuthInfo) error
|
||||
IsAdminPermitted(username string) bool
|
||||
|
||||
// GenSimpleToken produces a simple random string
|
||||
GenSimpleToken() (string, error)
|
||||
|
||||
// Revision gets current revision of authStore
|
||||
Revision() uint64
|
||||
|
||||
// CheckPassword checks a given pair of username and password is correct
|
||||
CheckPassword(username, password string) (uint64, error)
|
||||
|
||||
// Close does cleanup of AuthStore
|
||||
Close() error
|
||||
|
||||
// AuthInfoFromCtx gets AuthInfo from gRPC's context
|
||||
AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error)
|
||||
}
|
||||
|
||||
type authStore struct {
|
||||
@ -168,33 +136,11 @@ type authStore struct {
|
||||
|
||||
rangePermCache map[string]*unifiedRangePermissions // username -> unifiedRangePermissions
|
||||
|
||||
simpleTokensMu sync.RWMutex
|
||||
simpleTokens map[string]string // token -> username
|
||||
simpleTokenKeeper *simpleTokenTTLKeeper
|
||||
|
||||
revision uint64
|
||||
|
||||
indexWaiter func(uint64) <-chan struct{}
|
||||
}
|
||||
|
||||
func newDeleterFunc(as *authStore) func(string) {
|
||||
return func(t string) {
|
||||
as.simpleTokensMu.Lock()
|
||||
defer as.simpleTokensMu.Unlock()
|
||||
if username, ok := as.simpleTokens[t]; ok {
|
||||
plog.Infof("deleting token %s for user %s", t, username)
|
||||
delete(as.simpleTokens, t)
|
||||
}
|
||||
}
|
||||
simpleTokensMu sync.RWMutex
|
||||
simpleTokens map[string]string // token -> username
|
||||
}
|
||||
|
||||
func (as *authStore) AuthEnable() error {
|
||||
as.enabledMu.Lock()
|
||||
defer as.enabledMu.Unlock()
|
||||
if as.enabled {
|
||||
plog.Noticef("Authentication already enabled")
|
||||
return nil
|
||||
}
|
||||
b := as.be
|
||||
tx := b.BatchTx()
|
||||
tx.Lock()
|
||||
@ -214,64 +160,33 @@ func (as *authStore) AuthEnable() error {
|
||||
|
||||
tx.UnsafePut(authBucketName, enableFlagKey, authEnabled)
|
||||
|
||||
as.enabledMu.Lock()
|
||||
as.enabled = true
|
||||
|
||||
as.simpleTokenKeeper = NewSimpleTokenTTLKeeper(newDeleterFunc(as))
|
||||
as.enabledMu.Unlock()
|
||||
|
||||
as.rangePermCache = make(map[string]*unifiedRangePermissions)
|
||||
|
||||
as.revision = getRevision(tx)
|
||||
|
||||
plog.Noticef("Authentication enabled")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as *authStore) AuthDisable() {
|
||||
as.enabledMu.Lock()
|
||||
defer as.enabledMu.Unlock()
|
||||
if !as.enabled {
|
||||
return
|
||||
}
|
||||
b := as.be
|
||||
tx := b.BatchTx()
|
||||
tx.Lock()
|
||||
tx.UnsafePut(authBucketName, enableFlagKey, authDisabled)
|
||||
as.commitRevision(tx)
|
||||
tx.Unlock()
|
||||
b.ForceCommit()
|
||||
|
||||
as.enabledMu.Lock()
|
||||
as.enabled = false
|
||||
|
||||
as.simpleTokensMu.Lock()
|
||||
as.simpleTokens = make(map[string]string) // invalidate all tokens
|
||||
as.simpleTokensMu.Unlock()
|
||||
if as.simpleTokenKeeper != nil {
|
||||
as.simpleTokenKeeper.stop()
|
||||
as.simpleTokenKeeper = nil
|
||||
}
|
||||
as.enabledMu.Unlock()
|
||||
|
||||
plog.Noticef("Authentication disabled")
|
||||
}
|
||||
|
||||
func (as *authStore) Close() error {
|
||||
as.enabledMu.Lock()
|
||||
defer as.enabledMu.Unlock()
|
||||
if !as.enabled {
|
||||
return nil
|
||||
}
|
||||
if as.simpleTokenKeeper != nil {
|
||||
as.simpleTokenKeeper.stop()
|
||||
as.simpleTokenKeeper = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as *authStore) Authenticate(ctx context.Context, username, password string) (*pb.AuthenticateResponse, error) {
|
||||
if !as.isAuthEnabled() {
|
||||
return nil, ErrAuthNotEnabled
|
||||
}
|
||||
|
||||
// TODO(mitake): after adding jwt support, branching based on values of ctx is required
|
||||
index := ctx.Value("index").(uint64)
|
||||
simpleToken := ctx.Value("simpleToken").(string)
|
||||
@ -285,6 +200,11 @@ func (as *authStore) Authenticate(ctx context.Context, username, password string
|
||||
return nil, ErrAuthFailed
|
||||
}
|
||||
|
||||
if bcrypt.CompareHashAndPassword(user.Password, []byte(password)) != nil {
|
||||
plog.Noticef("authentication failed, invalid password for user %s", username)
|
||||
return &pb.AuthenticateResponse{}, ErrAuthFailed
|
||||
}
|
||||
|
||||
token := fmt.Sprintf("%s.%d", simpleToken, index)
|
||||
as.assignSimpleTokenToUser(username, token)
|
||||
|
||||
@ -292,24 +212,6 @@ func (as *authStore) Authenticate(ctx context.Context, username, password string
|
||||
return &pb.AuthenticateResponse{Token: token}, nil
|
||||
}
|
||||
|
||||
func (as *authStore) CheckPassword(username, password string) (uint64, error) {
|
||||
tx := as.be.BatchTx()
|
||||
tx.Lock()
|
||||
defer tx.Unlock()
|
||||
|
||||
user := getUser(tx, username)
|
||||
if user == nil {
|
||||
return 0, ErrAuthFailed
|
||||
}
|
||||
|
||||
if bcrypt.CompareHashAndPassword(user.Password, []byte(password)) != nil {
|
||||
plog.Noticef("authentication failed, invalid password for user %s", username)
|
||||
return 0, ErrAuthFailed
|
||||
}
|
||||
|
||||
return getRevision(tx), nil
|
||||
}
|
||||
|
||||
func (as *authStore) Recover(be backend.Backend) {
|
||||
enabled := false
|
||||
as.be = be
|
||||
@ -321,9 +223,6 @@ func (as *authStore) Recover(be backend.Backend) {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
as.revision = getRevision(tx)
|
||||
|
||||
tx.Unlock()
|
||||
|
||||
as.enabledMu.Lock()
|
||||
@ -332,11 +231,7 @@ func (as *authStore) Recover(be backend.Backend) {
|
||||
}
|
||||
|
||||
func (as *authStore) UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) {
|
||||
if len(r.Name) == 0 {
|
||||
return nil, ErrUserEmpty
|
||||
}
|
||||
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(r.Password), BcryptCost)
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(r.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
plog.Errorf("failed to hash password: %s", err)
|
||||
return nil, err
|
||||
@ -358,8 +253,6 @@ func (as *authStore) UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse,
|
||||
|
||||
putUser(tx, newUser)
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
plog.Noticef("added a new user: %s", r.Name)
|
||||
|
||||
return &pb.AuthUserAddResponse{}, nil
|
||||
@ -377,11 +270,6 @@ func (as *authStore) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDelete
|
||||
|
||||
delUser(tx, r.Name)
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
as.invalidateCachedPerm(r.Name)
|
||||
as.invalidateUser(r.Name)
|
||||
|
||||
plog.Noticef("deleted a user: %s", r.Name)
|
||||
|
||||
return &pb.AuthUserDeleteResponse{}, nil
|
||||
@ -390,7 +278,7 @@ func (as *authStore) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDelete
|
||||
func (as *authStore) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) {
|
||||
// TODO(mitake): measure the cost of bcrypt.GenerateFromPassword()
|
||||
// If the cost is too high, we should move the encryption to outside of the raft
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(r.Password), BcryptCost)
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(r.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
plog.Errorf("failed to hash password: %s", err)
|
||||
return nil, err
|
||||
@ -413,11 +301,6 @@ func (as *authStore) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*p
|
||||
|
||||
putUser(tx, updatedUser)
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
as.invalidateCachedPerm(r.Name)
|
||||
as.invalidateUser(r.Name)
|
||||
|
||||
plog.Noticef("changed a password of a user: %s", r.Name)
|
||||
|
||||
return &pb.AuthUserChangePasswordResponse{}, nil
|
||||
@ -453,8 +336,6 @@ func (as *authStore) UserGrantRole(r *pb.AuthUserGrantRoleRequest) (*pb.AuthUser
|
||||
|
||||
as.invalidateCachedPerm(r.User)
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
plog.Noticef("granted role %s to user %s", r.Role, r.User)
|
||||
return &pb.AuthUserGrantRoleResponse{}, nil
|
||||
}
|
||||
@ -470,7 +351,11 @@ func (as *authStore) UserGet(r *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse,
|
||||
if user == nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
resp.Roles = append(resp.Roles, user.Roles...)
|
||||
|
||||
for _, role := range user.Roles {
|
||||
resp.Roles = append(resp.Roles, role)
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
@ -519,8 +404,6 @@ func (as *authStore) UserRevokeRole(r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUs
|
||||
|
||||
as.invalidateCachedPerm(r.Name)
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
plog.Noticef("revoked role %s from user %s", r.Role, r.Name)
|
||||
return &pb.AuthUserRevokeRoleResponse{}, nil
|
||||
}
|
||||
@ -536,7 +419,11 @@ func (as *authStore) RoleGet(r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse,
|
||||
if role == nil {
|
||||
return nil, ErrRoleNotFound
|
||||
}
|
||||
resp.Perm = append(resp.Perm, role.KeyPermission...)
|
||||
|
||||
for _, perm := range role.KeyPermission {
|
||||
resp.Perm = append(resp.Perm, perm)
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
@ -586,8 +473,6 @@ func (as *authStore) RoleRevokePermission(r *pb.AuthRoleRevokePermissionRequest)
|
||||
// It should be optimized.
|
||||
as.clearCachedPerm()
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
plog.Noticef("revoked key %s from role %s", r.Key, r.Role)
|
||||
return &pb.AuthRoleRevokePermissionResponse{}, nil
|
||||
}
|
||||
@ -616,8 +501,6 @@ func (as *authStore) RoleDelete(r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDelete
|
||||
|
||||
delRole(tx, r.Role)
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
plog.Noticef("deleted role %s", r.Role)
|
||||
return &pb.AuthRoleDeleteResponse{}, nil
|
||||
}
|
||||
@ -638,21 +521,16 @@ func (as *authStore) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse,
|
||||
|
||||
putRole(tx, newRole)
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
plog.Noticef("Role %s is created", r.Name)
|
||||
|
||||
return &pb.AuthRoleAddResponse{}, nil
|
||||
}
|
||||
|
||||
func (as *authStore) AuthInfoFromToken(token string) (*AuthInfo, bool) {
|
||||
func (as *authStore) UsernameFromToken(token string) (string, bool) {
|
||||
as.simpleTokensMu.RLock()
|
||||
defer as.simpleTokensMu.RUnlock()
|
||||
t, ok := as.simpleTokens[token]
|
||||
if ok {
|
||||
as.simpleTokenKeeper.resetSimpleToken(token)
|
||||
}
|
||||
return &AuthInfo{Username: t, Revision: as.revision}, ok
|
||||
return t, ok
|
||||
}
|
||||
|
||||
type permSlice []*authpb.Permission
|
||||
@ -704,26 +582,15 @@ func (as *authStore) RoleGrantPermission(r *pb.AuthRoleGrantPermissionRequest) (
|
||||
// It should be optimized.
|
||||
as.clearCachedPerm()
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
plog.Noticef("role %s's permission of key %s is updated as %s", r.Name, r.Perm.Key, authpb.Permission_Type_name[int32(r.Perm.PermType)])
|
||||
|
||||
return &pb.AuthRoleGrantPermissionResponse{}, nil
|
||||
}
|
||||
|
||||
func (as *authStore) isOpPermitted(userName string, revision uint64, key, rangeEnd []byte, permTyp authpb.Permission_Type) error {
|
||||
func (as *authStore) isOpPermitted(userName string, key, rangeEnd []byte, permTyp authpb.Permission_Type) bool {
|
||||
// TODO(mitake): this function would be costly so we need a caching mechanism
|
||||
if !as.isAuthEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// only gets rev == 0 when passed AuthInfo{}; no user given
|
||||
if revision == 0 {
|
||||
return ErrUserEmpty
|
||||
}
|
||||
|
||||
if revision < as.revision {
|
||||
return ErrAuthOldRevision
|
||||
return true
|
||||
}
|
||||
|
||||
tx := as.be.BatchTx()
|
||||
@ -733,52 +600,48 @@ func (as *authStore) isOpPermitted(userName string, revision uint64, key, rangeE
|
||||
user := getUser(tx, userName)
|
||||
if user == nil {
|
||||
plog.Errorf("invalid user name %s for permission checking", userName)
|
||||
return ErrPermissionDenied
|
||||
return false
|
||||
}
|
||||
|
||||
// root role should have permission on all ranges
|
||||
if hasRootRole(user) {
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
|
||||
if as.isRangeOpPermitted(tx, userName, key, rangeEnd, permTyp) {
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
|
||||
return ErrPermissionDenied
|
||||
return false
|
||||
}
|
||||
|
||||
func (as *authStore) IsPutPermitted(authInfo *AuthInfo, key []byte) error {
|
||||
return as.isOpPermitted(authInfo.Username, authInfo.Revision, key, nil, authpb.WRITE)
|
||||
func (as *authStore) IsPutPermitted(username string, key []byte) bool {
|
||||
return as.isOpPermitted(username, key, nil, authpb.WRITE)
|
||||
}
|
||||
|
||||
func (as *authStore) IsRangePermitted(authInfo *AuthInfo, key, rangeEnd []byte) error {
|
||||
return as.isOpPermitted(authInfo.Username, authInfo.Revision, key, rangeEnd, authpb.READ)
|
||||
func (as *authStore) IsRangePermitted(username string, key, rangeEnd []byte) bool {
|
||||
return as.isOpPermitted(username, key, rangeEnd, authpb.READ)
|
||||
}
|
||||
|
||||
func (as *authStore) IsDeleteRangePermitted(authInfo *AuthInfo, key, rangeEnd []byte) error {
|
||||
return as.isOpPermitted(authInfo.Username, authInfo.Revision, key, rangeEnd, authpb.WRITE)
|
||||
func (as *authStore) IsDeleteRangePermitted(username string, key, rangeEnd []byte) bool {
|
||||
return as.isOpPermitted(username, key, rangeEnd, authpb.WRITE)
|
||||
}
|
||||
|
||||
func (as *authStore) IsAdminPermitted(authInfo *AuthInfo) error {
|
||||
func (as *authStore) IsAdminPermitted(username string) bool {
|
||||
if !as.isAuthEnabled() {
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
|
||||
tx := as.be.BatchTx()
|
||||
tx.Lock()
|
||||
defer tx.Unlock()
|
||||
|
||||
u := getUser(tx, authInfo.Username)
|
||||
u := getUser(tx, username)
|
||||
if u == nil {
|
||||
return ErrUserNotFound
|
||||
return false
|
||||
}
|
||||
|
||||
if !hasRootRole(u) {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
return hasRootRole(u)
|
||||
}
|
||||
|
||||
func getUser(tx backend.BatchTx, username string) *authpb.User {
|
||||
@ -882,7 +745,7 @@ func (as *authStore) isAuthEnabled() bool {
|
||||
return as.enabled
|
||||
}
|
||||
|
||||
func NewAuthStore(be backend.Backend, indexWaiter func(uint64) <-chan struct{}) *authStore {
|
||||
func NewAuthStore(be backend.Backend) *authStore {
|
||||
tx := be.BatchTx()
|
||||
tx.Lock()
|
||||
|
||||
@ -890,33 +753,13 @@ func NewAuthStore(be backend.Backend, indexWaiter func(uint64) <-chan struct{})
|
||||
tx.UnsafeCreateBucket(authUsersBucketName)
|
||||
tx.UnsafeCreateBucket(authRolesBucketName)
|
||||
|
||||
enabled := false
|
||||
_, vs := tx.UnsafeRange(authBucketName, enableFlagKey, nil, 0)
|
||||
if len(vs) == 1 {
|
||||
if bytes.Equal(vs[0], authEnabled) {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
as := &authStore{
|
||||
be: be,
|
||||
simpleTokens: make(map[string]string),
|
||||
revision: getRevision(tx),
|
||||
indexWaiter: indexWaiter,
|
||||
enabled: enabled,
|
||||
rangePermCache: make(map[string]*unifiedRangePermissions),
|
||||
}
|
||||
|
||||
if enabled {
|
||||
as.simpleTokenKeeper = NewSimpleTokenTTLKeeper(newDeleterFunc(as))
|
||||
}
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
tx.Unlock()
|
||||
be.ForceCommit()
|
||||
|
||||
return as
|
||||
return &authStore{
|
||||
be: be,
|
||||
simpleTokens: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func hasRootRole(u *authpb.User) bool {
|
||||
@ -927,67 +770,3 @@ func hasRootRole(u *authpb.User) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (as *authStore) commitRevision(tx backend.BatchTx) {
|
||||
as.revision++
|
||||
revBytes := make([]byte, revBytesLen)
|
||||
binary.BigEndian.PutUint64(revBytes, as.revision)
|
||||
tx.UnsafePut(authBucketName, revisionKey, revBytes)
|
||||
}
|
||||
|
||||
func getRevision(tx backend.BatchTx) uint64 {
|
||||
_, vs := tx.UnsafeRange(authBucketName, []byte(revisionKey), nil, 0)
|
||||
if len(vs) != 1 {
|
||||
// this can happen in the initialization phase
|
||||
return 0
|
||||
}
|
||||
|
||||
return binary.BigEndian.Uint64(vs[0])
|
||||
}
|
||||
|
||||
func (as *authStore) Revision() uint64 {
|
||||
return as.revision
|
||||
}
|
||||
|
||||
func (as *authStore) isValidSimpleToken(token string, ctx context.Context) bool {
|
||||
splitted := strings.Split(token, ".")
|
||||
if len(splitted) != 2 {
|
||||
return false
|
||||
}
|
||||
index, err := strconv.Atoi(splitted[1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
select {
|
||||
case <-as.indexWaiter(uint64(index)):
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (as *authStore) AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error) {
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ts, tok := md["token"]
|
||||
if !tok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
token := ts[0]
|
||||
if !as.isValidSimpleToken(token, ctx) {
|
||||
return nil, ErrInvalidAuthToken
|
||||
}
|
||||
|
||||
authInfo, uok := as.AuthInfoFromToken(token)
|
||||
if !uok {
|
||||
plog.Warningf("invalid auth token: %s", token)
|
||||
return nil, ErrInvalidAuthToken
|
||||
}
|
||||
return authInfo, nil
|
||||
}
|
||||
|
@ -20,20 +20,9 @@ import (
|
||||
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/mvcc/backend"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func init() { BcryptCost = bcrypt.MinCost }
|
||||
|
||||
func dummyIndexWaiter(index uint64) <-chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
ch <- struct{}{}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func TestUserAdd(t *testing.T) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
defer func() {
|
||||
@ -41,7 +30,7 @@ func TestUserAdd(t *testing.T) {
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b, dummyIndexWaiter)
|
||||
as := NewAuthStore(b)
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo"}
|
||||
_, err := as.UserAdd(ua) // add a non-existing user
|
||||
if err != nil {
|
||||
@ -54,55 +43,26 @@ func TestUserAdd(t *testing.T) {
|
||||
if err != ErrUserAlreadyExist {
|
||||
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
|
||||
}
|
||||
|
||||
ua = &pb.AuthUserAddRequest{Name: ""}
|
||||
_, err = as.UserAdd(ua) // add a user with empty name
|
||||
if err != ErrUserEmpty {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func enableAuthAndCreateRoot(as *authStore) error {
|
||||
_, err := as.UserAdd(&pb.AuthUserAddRequest{Name: "root", Password: "root"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: "root"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "root", Role: "root"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return as.AuthEnable()
|
||||
}
|
||||
|
||||
func TestCheckPassword(t *testing.T) {
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
defer func() {
|
||||
b.Close()
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b, dummyIndexWaiter)
|
||||
defer as.Close()
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as := NewAuthStore(b)
|
||||
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo", Password: "bar"}
|
||||
_, err = as.UserAdd(ua)
|
||||
_, err := as.UserAdd(ua)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// auth a non-existing user
|
||||
_, err = as.CheckPassword("foo-test", "bar")
|
||||
ctx1 := context.WithValue(context.WithValue(context.TODO(), "index", uint64(1)), "simpleToken", "dummy")
|
||||
_, err = as.Authenticate(ctx1, "foo-test", "bar")
|
||||
if err == nil {
|
||||
t.Fatalf("expected %v, got %v", ErrAuthFailed, err)
|
||||
}
|
||||
@ -111,13 +71,15 @@ func TestCheckPassword(t *testing.T) {
|
||||
}
|
||||
|
||||
// auth an existing user with correct password
|
||||
_, err = as.CheckPassword("foo", "bar")
|
||||
ctx2 := context.WithValue(context.WithValue(context.TODO(), "index", uint64(2)), "simpleToken", "dummy")
|
||||
_, err = as.Authenticate(ctx2, "foo", "bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// auth an existing user but with wrong password
|
||||
_, err = as.CheckPassword("foo", "")
|
||||
ctx3 := context.WithValue(context.WithValue(context.TODO(), "index", uint64(3)), "simpleToken", "dummy")
|
||||
_, err = as.Authenticate(ctx3, "foo", "")
|
||||
if err == nil {
|
||||
t.Fatalf("expected %v, got %v", ErrAuthFailed, err)
|
||||
}
|
||||
@ -133,15 +95,10 @@ func TestUserDelete(t *testing.T) {
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b, dummyIndexWaiter)
|
||||
defer as.Close()
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as := NewAuthStore(b)
|
||||
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo"}
|
||||
_, err = as.UserAdd(ua)
|
||||
_, err := as.UserAdd(ua)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -170,14 +127,9 @@ func TestUserChangePassword(t *testing.T) {
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b, dummyIndexWaiter)
|
||||
defer as.Close()
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as := NewAuthStore(b)
|
||||
|
||||
_, err = as.UserAdd(&pb.AuthUserAddRequest{Name: "foo"})
|
||||
_, err := as.UserAdd(&pb.AuthUserAddRequest{Name: "foo"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -216,15 +168,10 @@ func TestRoleAdd(t *testing.T) {
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b, dummyIndexWaiter)
|
||||
defer as.Close()
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as := NewAuthStore(b)
|
||||
|
||||
// adds a new role
|
||||
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test"})
|
||||
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -237,14 +184,9 @@ func TestUserGrant(t *testing.T) {
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b, dummyIndexWaiter)
|
||||
defer as.Close()
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as := NewAuthStore(b)
|
||||
|
||||
_, err = as.UserAdd(&pb.AuthUserAddRequest{Name: "foo"})
|
||||
_, err := as.UserAdd(&pb.AuthUserAddRequest{Name: "foo"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -269,93 +211,4 @@ func TestUserGrant(t *testing.T) {
|
||||
if err != ErrUserNotFound {
|
||||
t.Fatalf("expected %v, got %v", ErrUserNotFound, err)
|
||||
}
|
||||
|
||||
// non-admin user
|
||||
err = as.IsAdminPermitted(&AuthInfo{Username: "foo", Revision: 1})
|
||||
if err != ErrPermissionDenied {
|
||||
t.Errorf("expected %v, got %v", ErrPermissionDenied, err)
|
||||
}
|
||||
|
||||
// disabled auth should return nil
|
||||
as.AuthDisable()
|
||||
err = as.IsAdminPermitted(&AuthInfo{Username: "root", Revision: 1})
|
||||
if err != nil {
|
||||
t.Errorf("expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverFromSnapshot(t *testing.T) {
|
||||
as, _ := setupAuthStore(t)
|
||||
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo"}
|
||||
_, err := as.UserAdd(ua) // add an existing user
|
||||
if err == nil {
|
||||
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
|
||||
}
|
||||
if err != ErrUserAlreadyExist {
|
||||
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
|
||||
}
|
||||
|
||||
ua = &pb.AuthUserAddRequest{Name: ""}
|
||||
_, err = as.UserAdd(ua) // add a user with empty name
|
||||
if err != ErrUserEmpty {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
as.Close()
|
||||
|
||||
as2 := NewAuthStore(as.be, dummyIndexWaiter)
|
||||
defer func(a *authStore) {
|
||||
a.Close()
|
||||
}(as2)
|
||||
|
||||
if !as2.isAuthEnabled() {
|
||||
t.Fatal("recovering authStore from existing backend failed")
|
||||
}
|
||||
|
||||
ul, err := as.UserList(&pb.AuthUserListRequest{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !contains(ul.Users, "root") {
|
||||
t.Errorf("expected %v in %v", "root", ul.Users)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(array []string, str string) bool {
|
||||
for _, s := range array {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setupAuthStore(t *testing.T) (store *authStore, teardownfunc func(t *testing.T)) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
|
||||
as := NewAuthStore(b, dummyIndexWaiter)
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// adds a new role
|
||||
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo", Password: "bar"}
|
||||
_, err = as.UserAdd(ua) // add a non-existing user
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tearDown := func(t *testing.T) {
|
||||
b.Close()
|
||||
os.Remove(tPath)
|
||||
as.Close()
|
||||
}
|
||||
return as, tearDown
|
||||
}
|
||||
|
39
build
39
build
@ -4,19 +4,15 @@
|
||||
ORG_PATH="github.com/coreos"
|
||||
REPO_PATH="${ORG_PATH}/etcd"
|
||||
export GO15VENDOREXPERIMENT="1"
|
||||
|
||||
eval $(go env)
|
||||
GIT_SHA=`git rev-parse --short HEAD || echo "GitNotFound"`
|
||||
if [ ! -z "$FAILPOINTS" ]; then
|
||||
GIT_SHA="$GIT_SHA"-FAILPOINTS
|
||||
fi
|
||||
|
||||
# Set GO_LDFLAGS="-s" for building without symbols for debugging.
|
||||
GO_LDFLAGS="$GO_LDFLAGS -X ${REPO_PATH}/cmd/vendor/${REPO_PATH}/version.GitSHA=${GIT_SHA}"
|
||||
|
||||
# enable/disable failpoints
|
||||
toggle_failpoints() {
|
||||
FAILPKGS="etcdserver/ mvcc/backend/"
|
||||
FAILPKGS="etcdserver/"
|
||||
|
||||
mode="disable"
|
||||
if [ ! -z "$FAILPOINTS" ]; then mode="enable"; fi
|
||||
@ -31,33 +27,18 @@ toggle_failpoints() {
|
||||
}
|
||||
|
||||
etcd_build() {
|
||||
out="bin"
|
||||
if [ -n "${BINDIR}" ]; then out="${BINDIR}"; fi
|
||||
if [ -z "${GOARCH}" ] || [ "${GOARCH}" = "$(go env GOHOSTARCH)" ]; then
|
||||
out="bin"
|
||||
else
|
||||
out="bin/${GOARCH}"
|
||||
fi
|
||||
toggle_failpoints
|
||||
# Static compilation is useful when etcd is run in a container
|
||||
CGO_ENABLED=0 go build $GO_BUILD_FLAGS -installsuffix cgo -ldflags "$GO_LDFLAGS" -o ${out}/etcd ${REPO_PATH}/cmd/etcd || return
|
||||
CGO_ENABLED=0 go build $GO_BUILD_FLAGS -installsuffix cgo -ldflags "$GO_LDFLAGS" -o ${out}/etcdctl ${REPO_PATH}/cmd/etcdctl || return
|
||||
}
|
||||
|
||||
etcd_setup_gopath() {
|
||||
CDIR=$(cd `dirname "$0"` && pwd)
|
||||
cd "$CDIR"
|
||||
etcdGOPATH=${CDIR}/gopath
|
||||
# preserve old gopath to support building with unvendored tooling deps (e.g., gofail)
|
||||
if [ -n "$GOPATH" ]; then
|
||||
GOPATH=":$GOPATH"
|
||||
fi
|
||||
export GOPATH=${etcdGOPATH}$GOPATH
|
||||
rm -rf ${etcdGOPATH}/src
|
||||
mkdir -p ${etcdGOPATH}
|
||||
ln -s ${CDIR}/cmd/vendor ${etcdGOPATH}/src
|
||||
CGO_ENABLED=0 go build $GO_BUILD_FLAGS -installsuffix cgo -ldflags "-s -X ${REPO_PATH}/cmd/vendor/${REPO_PATH}/version.GitSHA=${GIT_SHA}" -o ${out}/etcd ${REPO_PATH}/cmd
|
||||
CGO_ENABLED=0 go build $GO_BUILD_FLAGS -installsuffix cgo -ldflags "-s" -o ${out}/etcdctl ${REPO_PATH}/cmd/etcdctl
|
||||
}
|
||||
|
||||
toggle_failpoints
|
||||
|
||||
# only build when called directly, not sourced
|
||||
if echo "$0" | grep "build$" >/dev/null; then
|
||||
# force new gopath so builds outside of gopath work
|
||||
etcd_setup_gopath
|
||||
etcd_build
|
||||
fi
|
||||
# don't build when sourced
|
||||
(echo "$0" | grep "/build$" > /dev/null) && etcd_build || true
|
||||
|
64
build.ps1
64
build.ps1
@ -1,18 +1,9 @@
|
||||
$ORG_PATH="github.com/coreos"
|
||||
$REPO_PATH="$ORG_PATH/etcd"
|
||||
$PWD = $((Get-Item -Path ".\" -Verbose).FullName)
|
||||
$FSROOT = $((Get-Location).Drive.Name+":")
|
||||
$FSYS = $((Get-WMIObject win32_logicaldisk -filter "DeviceID = '$FSROOT'").filesystem)
|
||||
|
||||
if ($FSYS.StartsWith("FAT","CurrentCultureIgnoreCase")) {
|
||||
echo "Error: Cannot build etcd using the $FSYS filesystem (use NTFS instead)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Set $Env:GO_LDFLAGS="-s" for building without symbols.
|
||||
$GO_LDFLAGS="$Env:GO_LDFLAGS -X $REPO_PATH/cmd/vendor/$REPO_PATH/version.GitSHA=$GIT_SHA"
|
||||
|
||||
# rebuild symlinks
|
||||
echo "Rebuilding symlinks"
|
||||
git ls-files -s cmd | select-string -pattern 120000 | ForEach {
|
||||
$l = $_.ToString()
|
||||
$lnkname = $l.Split(' ')[1]
|
||||
@ -22,54 +13,27 @@ git ls-files -s cmd | select-string -pattern 120000 | ForEach {
|
||||
|
||||
$terms = $lnkname.Split("\")
|
||||
$dirname = $terms[0..($terms.length-2)] -join "\"
|
||||
|
||||
$lnkname = "$PWD\$lnkname"
|
||||
$targetAbs = "$((Get-Item -Path "$dirname\$target").FullName)"
|
||||
$targetAbs = $targetAbs.Replace("/", "\")
|
||||
|
||||
if (test-path -pathtype container "$targetAbs") {
|
||||
if (Test-Path "$lnkname") {
|
||||
if ((Get-Item "$lnkname") -is [System.IO.DirectoryInfo]) {
|
||||
# rd so deleting junction doesn't take files with it
|
||||
cmd /c rd "$lnkname"
|
||||
}
|
||||
}
|
||||
if (Test-Path "$lnkname") {
|
||||
if (!((Get-Item "$lnkname") -is [System.IO.DirectoryInfo])) {
|
||||
cmd /c del /A /F "$lnkname"
|
||||
}
|
||||
}
|
||||
cmd /c mklink /J "$lnkname" "$targetAbs" ">NUL"
|
||||
# rd so deleting junction doesn't take files with it
|
||||
cmd /c rd "$lnkname"
|
||||
cmd /c del /A /F "$lnkname"
|
||||
cmd /c mklink /J "$lnkname" "$targetAbs"
|
||||
} else {
|
||||
# Remove file with symlink data (first run)
|
||||
if (Test-Path "$lnkname") {
|
||||
cmd /c del /A /F "$lnkname"
|
||||
}
|
||||
cmd /c mklink /H "$lnkname" "$targetAbs" ">NUL"
|
||||
cmd /c del /A /F "$lnkname"
|
||||
cmd /c mklink /H "$lnkname" "$targetAbs"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $env:GOPATH) {
|
||||
$orgpath="$PWD\gopath\src\" + $ORG_PATH.Replace("/", "\")
|
||||
if (Test-Path "$orgpath\etcd") {
|
||||
if ((Get-Item "$orgpath\etcd") -is [System.IO.DirectoryInfo]) {
|
||||
# rd so deleting junction doesn't take files with it
|
||||
cmd /c rd "$orgpath\etcd"
|
||||
}
|
||||
}
|
||||
if (Test-Path "$orgpath") {
|
||||
if ((Get-Item "$orgpath") -is [System.IO.DirectoryInfo]) {
|
||||
# rd so deleting junction doesn't take files with it
|
||||
cmd /c rd "$orgpath"
|
||||
}
|
||||
}
|
||||
if (Test-Path "$orgpath") {
|
||||
if (!((Get-Item "$orgpath") -is [System.IO.DirectoryInfo])) {
|
||||
# Remove file with symlink data (first run)
|
||||
cmd /c del /A /F "$orgpath"
|
||||
}
|
||||
}
|
||||
cmd /c mkdir "$orgpath"
|
||||
cmd /c mklink /J "$orgpath\etcd" "$PWD" ">NUL"
|
||||
cmd /c rd "$orgpath\etcd"
|
||||
cmd /c del "$orgpath"
|
||||
cmd /c mkdir "$orgpath"
|
||||
cmd /c mklink /J "$orgpath\etcd" "$PWD"
|
||||
$env:GOPATH = "$PWD\gopath"
|
||||
}
|
||||
|
||||
@ -77,5 +41,5 @@ if (-not $env:GOPATH) {
|
||||
$env:CGO_ENABLED = 0
|
||||
$env:GO15VENDOREXPERIMENT = 1
|
||||
$GIT_SHA="$(git rev-parse --short HEAD)"
|
||||
go build -a -installsuffix cgo -ldflags $GO_LDFLAGS -o bin\etcd.exe "$REPO_PATH\cmd\etcd"
|
||||
go build -a -installsuffix cgo -ldflags $GO_LDFLAGS -o bin\etcdctl.exe "$REPO_PATH\cmd\etcdctl"
|
||||
go build -a -installsuffix cgo -ldflags "-s -X $REPO_PATH/cmd/vendor/$REPO_PATH/version.GitSHA=$GIT_SHA" -o bin\etcd.exe "$REPO_PATH\cmd"
|
||||
go build -a -installsuffix cgo -ldflags "-s" -o bin\etcdctl.exe "$REPO_PATH\cmd\etcdctl"
|
||||
|
@ -114,4 +114,4 @@ if err != nil {
|
||||
|
||||
3. Default etcd/client cannot handle the case that the remote server is SIGSTOPed now. TCP keepalive mechanism doesn't help in this scenario because operating system may still send TCP keep-alive packets. Over time we'd like to improve this functionality, but solving this issue isn't high priority because a real-life case in which a server is stopped, but the connection is kept alive, hasn't been brought to our attention.
|
||||
|
||||
4. etcd/client cannot detect whether a member is healthy with watches and non-quorum read requests. If the member is isolated from the cluster, etcd/client may retrieve outdated data. Instead, users can either issue quorum read requests or monitor the /health endpoint for member health information.
|
||||
4. etcd/client cannot detect whether the member in use is healthy when doing read requests. If the member is isolated from the cluster, etcd/client may retrieve outdated data. As a workaround, users could monitor experimental /health endpoint for member healthy information. We are improving it at [#3265](https://github.com/coreos/etcd/issues/3265).
|
||||
|
147
client/client.go
147
client/client.go
@ -22,6 +22,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
@ -260,67 +261,53 @@ type httpClusterClient struct {
|
||||
selectionMode EndpointSelectionMode
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) getLeaderEndpoint(ctx context.Context, eps []url.URL) (string, error) {
|
||||
ceps := make([]url.URL, len(eps))
|
||||
copy(ceps, eps)
|
||||
|
||||
// To perform a lookup on the new endpoint list without using the current
|
||||
// client, we'll copy it
|
||||
clientCopy := &httpClusterClient{
|
||||
clientFactory: c.clientFactory,
|
||||
credentials: c.credentials,
|
||||
rand: c.rand,
|
||||
|
||||
pinned: 0,
|
||||
endpoints: ceps,
|
||||
}
|
||||
|
||||
mAPI := NewMembersAPI(clientCopy)
|
||||
leader, err := mAPI.Leader(ctx)
|
||||
func (c *httpClusterClient) getLeaderEndpoint() (string, error) {
|
||||
mAPI := NewMembersAPI(c)
|
||||
leader, err := mAPI.Leader(context.Background())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(leader.ClientURLs) == 0 {
|
||||
return "", ErrNoLeaderEndpoint
|
||||
}
|
||||
|
||||
return leader.ClientURLs[0], nil // TODO: how to handle multiple client URLs?
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) parseEndpoints(eps []string) ([]url.URL, error) {
|
||||
func (c *httpClusterClient) SetEndpoints(eps []string) error {
|
||||
if len(eps) == 0 {
|
||||
return []url.URL{}, ErrNoEndpoints
|
||||
return ErrNoEndpoints
|
||||
}
|
||||
|
||||
neps := make([]url.URL, len(eps))
|
||||
for i, ep := range eps {
|
||||
u, err := url.Parse(ep)
|
||||
if err != nil {
|
||||
return []url.URL{}, err
|
||||
return err
|
||||
}
|
||||
neps[i] = *u
|
||||
}
|
||||
return neps, nil
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) SetEndpoints(eps []string) error {
|
||||
neps, err := c.parseEndpoints(eps)
|
||||
if err != nil {
|
||||
return err
|
||||
switch c.selectionMode {
|
||||
case EndpointSelectionRandom:
|
||||
c.endpoints = shuffleEndpoints(c.rand, neps)
|
||||
c.pinned = 0
|
||||
case EndpointSelectionPrioritizeLeader:
|
||||
c.endpoints = neps
|
||||
lep, err := c.getLeaderEndpoint()
|
||||
if err != nil {
|
||||
return ErrNoLeaderEndpoint
|
||||
}
|
||||
|
||||
for i := range c.endpoints {
|
||||
if c.endpoints[i].String() == lep {
|
||||
c.pinned = i
|
||||
break
|
||||
}
|
||||
}
|
||||
// If endpoints doesn't have the lu, just keep c.pinned = 0.
|
||||
// Forwarding between follower and leader would be required but it works.
|
||||
default:
|
||||
return errors.New(fmt.Sprintf("invalid endpoint selection mode: %d", c.selectionMode))
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.endpoints = shuffleEndpoints(c.rand, neps)
|
||||
// We're not doing anything for PrioritizeLeader here. This is
|
||||
// due to not having a context meaning we can't call getLeaderEndpoint
|
||||
// However, if you're using PrioritizeLeader, you've already been told
|
||||
// to regularly call sync, where we do have a ctx, and can figure the
|
||||
// leader. PrioritizeLeader is also quite a loose guarantee, so deal
|
||||
// with it
|
||||
c.pinned = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -414,51 +401,27 @@ func (c *httpClusterClient) Sync(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var eps []string
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
eps := make([]string, 0)
|
||||
for _, m := range ms {
|
||||
eps = append(eps, m.ClientURLs...)
|
||||
}
|
||||
sort.Sort(sort.StringSlice(eps))
|
||||
|
||||
neps, err := c.parseEndpoints(eps)
|
||||
if err != nil {
|
||||
return err
|
||||
ceps := make([]string, len(c.endpoints))
|
||||
for i, cep := range c.endpoints {
|
||||
ceps[i] = cep.String()
|
||||
}
|
||||
sort.Sort(sort.StringSlice(ceps))
|
||||
// fast path if no change happens
|
||||
// this helps client to pin the endpoint when no cluster change
|
||||
if reflect.DeepEqual(eps, ceps) {
|
||||
return nil
|
||||
}
|
||||
|
||||
npin := 0
|
||||
|
||||
switch c.selectionMode {
|
||||
case EndpointSelectionRandom:
|
||||
c.RLock()
|
||||
eq := endpointsEqual(c.endpoints, neps)
|
||||
c.RUnlock()
|
||||
|
||||
if eq {
|
||||
return nil
|
||||
}
|
||||
// When items in the endpoint list changes, we choose a new pin
|
||||
neps = shuffleEndpoints(c.rand, neps)
|
||||
case EndpointSelectionPrioritizeLeader:
|
||||
nle, err := c.getLeaderEndpoint(ctx, neps)
|
||||
if err != nil {
|
||||
return ErrNoLeaderEndpoint
|
||||
}
|
||||
|
||||
for i, n := range neps {
|
||||
if n.String() == nle {
|
||||
npin = i
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid endpoint selection mode: %d", c.selectionMode)
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.endpoints = neps
|
||||
c.pinned = npin
|
||||
|
||||
return nil
|
||||
return c.SetEndpoints(eps)
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) AutoSync(ctx context.Context, interval time.Duration) error {
|
||||
@ -644,27 +607,3 @@ func shuffleEndpoints(r *rand.Rand, eps []url.URL) []url.URL {
|
||||
}
|
||||
return neps
|
||||
}
|
||||
|
||||
func endpointsEqual(left, right []url.URL) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
|
||||
sLeft := make([]string, len(left))
|
||||
sRight := make([]string, len(right))
|
||||
for i, l := range left {
|
||||
sLeft[i] = l.String()
|
||||
}
|
||||
for i, r := range right {
|
||||
sRight[i] = r.String()
|
||||
}
|
||||
|
||||
sort.Strings(sLeft)
|
||||
sort.Strings(sRight)
|
||||
for i := range sLeft {
|
||||
if sLeft[i] != sRight[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -855,7 +855,7 @@ func TestHTTPClusterClientAutoSyncFail(t *testing.T) {
|
||||
}
|
||||
|
||||
err = hc.AutoSync(context.Background(), time.Hour)
|
||||
if !strings.HasPrefix(err.Error(), ErrClusterUnavailable.Error()) {
|
||||
if err.Error() != ErrClusterUnavailable.Error() {
|
||||
t.Fatalf("incorrect error value: want=%v got=%v", ErrClusterUnavailable, err)
|
||||
}
|
||||
}
|
||||
@ -900,90 +900,6 @@ func TestHTTPClusterClientSyncPinEndpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTTPClusterClientSyncUnpinEndpoint tests that Sync() unpins the endpoint when
|
||||
// it gets a different member list than before.
|
||||
func TestHTTPClusterClientSyncUnpinEndpoint(t *testing.T) {
|
||||
cf := newStaticHTTPClientFactory([]staticHTTPResponse{
|
||||
{
|
||||
resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
|
||||
body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
|
||||
},
|
||||
{
|
||||
resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
|
||||
body: []byte(`{"members":[{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
|
||||
},
|
||||
{
|
||||
resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
|
||||
body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
|
||||
},
|
||||
})
|
||||
|
||||
hc := &httpClusterClient{
|
||||
clientFactory: cf,
|
||||
rand: rand.New(rand.NewSource(0)),
|
||||
}
|
||||
err := hc.SetEndpoints([]string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error during setup: %#v", err)
|
||||
}
|
||||
wants := []string{"http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
err = hc.Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("#%d: unexpected error during Sync: %#v", i, err)
|
||||
}
|
||||
|
||||
if g := hc.endpoints[hc.pinned]; g.String() != wants[i] {
|
||||
t.Errorf("#%d: pinned endpoint = %v, want %v", i, g, wants[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTTPClusterClientSyncPinLeaderEndpoint tests that Sync() pins the leader
|
||||
// when the selection mode is EndpointSelectionPrioritizeLeader
|
||||
func TestHTTPClusterClientSyncPinLeaderEndpoint(t *testing.T) {
|
||||
cf := newStaticHTTPClientFactory([]staticHTTPResponse{
|
||||
{
|
||||
resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
|
||||
body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
|
||||
},
|
||||
{
|
||||
resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
|
||||
body: []byte(`{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]}`),
|
||||
},
|
||||
{
|
||||
resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
|
||||
body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
|
||||
},
|
||||
{
|
||||
resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
|
||||
body: []byte(`{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}`),
|
||||
},
|
||||
})
|
||||
|
||||
hc := &httpClusterClient{
|
||||
clientFactory: cf,
|
||||
rand: rand.New(rand.NewSource(0)),
|
||||
selectionMode: EndpointSelectionPrioritizeLeader,
|
||||
endpoints: []url.URL{{}}, // Need somewhere to pretend to send to initially
|
||||
}
|
||||
|
||||
wants := []string{"http://127.0.0.1:4003", "http://127.0.0.1:4002"}
|
||||
|
||||
for i, want := range wants {
|
||||
err := hc.Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("#%d: unexpected error during Sync: %#v", i, err)
|
||||
}
|
||||
|
||||
pinned := hc.endpoints[hc.pinned].String()
|
||||
if pinned != want {
|
||||
t.Errorf("#%d: pinned endpoint = %v, want %v", i, pinned, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClusterClientResetFail(t *testing.T) {
|
||||
tests := [][]string{
|
||||
// need at least one endpoint
|
||||
|
@ -21,11 +21,7 @@ type ClusterError struct {
|
||||
}
|
||||
|
||||
func (ce *ClusterError) Error() string {
|
||||
s := ErrClusterUnavailable.Error()
|
||||
for i, e := range ce.Errors {
|
||||
s += fmt.Sprintf("; error #%d: %s\n", i, e)
|
||||
}
|
||||
return s
|
||||
return ErrClusterUnavailable.Error()
|
||||
}
|
||||
|
||||
func (ce *ClusterError) Detail() string {
|
||||
|
@ -1,17 +0,0 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package integration implements tests built upon embedded etcd, focusing on
|
||||
// the correctness of the etcd v2 client.
|
||||
package integration
|
File diff suppressed because it is too large
Load Diff
@ -191,10 +191,6 @@ type SetOptions struct {
|
||||
|
||||
// Dir specifies whether or not this Node should be created as a directory.
|
||||
Dir bool
|
||||
|
||||
// NoValueOnSuccess specifies whether the response contains the current value of the Node.
|
||||
// If set, the response will only contain the current value when the request fails.
|
||||
NoValueOnSuccess bool
|
||||
}
|
||||
|
||||
type GetOptions struct {
|
||||
@ -272,10 +268,6 @@ type Response struct {
|
||||
// Index holds the cluster-level index at the time the Response was generated.
|
||||
// This index is not tied to the Node(s) contained in this Response.
|
||||
Index uint64 `json:"-"`
|
||||
|
||||
// ClusterID holds the cluster-level ID reported by the server. This
|
||||
// should be different for different etcd clusters.
|
||||
ClusterID string `json:"-"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
@ -343,7 +335,6 @@ func (k *httpKeysAPI) Set(ctx context.Context, key, val string, opts *SetOptions
|
||||
act.TTL = opts.TTL
|
||||
act.Refresh = opts.Refresh
|
||||
act.Dir = opts.Dir
|
||||
act.NoValueOnSuccess = opts.NoValueOnSuccess
|
||||
}
|
||||
|
||||
doCtx := ctx
|
||||
@ -532,16 +523,15 @@ func (w *waitAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
}
|
||||
|
||||
type setAction struct {
|
||||
Prefix string
|
||||
Key string
|
||||
Value string
|
||||
PrevValue string
|
||||
PrevIndex uint64
|
||||
PrevExist PrevExistType
|
||||
TTL time.Duration
|
||||
Refresh bool
|
||||
Dir bool
|
||||
NoValueOnSuccess bool
|
||||
Prefix string
|
||||
Key string
|
||||
Value string
|
||||
PrevValue string
|
||||
PrevIndex uint64
|
||||
PrevExist PrevExistType
|
||||
TTL time.Duration
|
||||
Refresh bool
|
||||
Dir bool
|
||||
}
|
||||
|
||||
func (a *setAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
@ -575,9 +565,6 @@ func (a *setAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
if a.Refresh {
|
||||
form.Add("refresh", "true")
|
||||
}
|
||||
if a.NoValueOnSuccess {
|
||||
params.Set("noValueOnSuccess", strconv.FormatBool(a.NoValueOnSuccess))
|
||||
}
|
||||
|
||||
u.RawQuery = params.Encode()
|
||||
body := strings.NewReader(form.Encode())
|
||||
@ -669,7 +656,6 @@ func unmarshalSuccessfulKeysResponse(header http.Header, body []byte) (*Response
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
res.ClusterID = header.Get("X-Etcd-Cluster-ID")
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
|
@ -407,15 +407,6 @@ func TestSetAction(t *testing.T) {
|
||||
wantURL: "http://example.com/foo?dir=true",
|
||||
wantBody: "",
|
||||
},
|
||||
// NoValueOnSuccess is set
|
||||
{
|
||||
act: setAction{
|
||||
Key: "foo",
|
||||
NoValueOnSuccess: true,
|
||||
},
|
||||
wantURL: "http://example.com/foo?noValueOnSuccess=true",
|
||||
wantBody: "value=",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
@ -673,24 +664,23 @@ func TestUnmarshalSuccessfulResponse(t *testing.T) {
|
||||
expiration.UnmarshalText([]byte("2015-04-07T04:40:23.044979686Z"))
|
||||
|
||||
tests := []struct {
|
||||
indexHdr string
|
||||
clusterIDHdr string
|
||||
body string
|
||||
wantRes *Response
|
||||
wantErr bool
|
||||
hdr string
|
||||
body string
|
||||
wantRes *Response
|
||||
wantErr bool
|
||||
}{
|
||||
// Neither PrevNode or Node
|
||||
{
|
||||
indexHdr: "1",
|
||||
body: `{"action":"delete"}`,
|
||||
wantRes: &Response{Action: "delete", Index: 1},
|
||||
wantErr: false,
|
||||
hdr: "1",
|
||||
body: `{"action":"delete"}`,
|
||||
wantRes: &Response{Action: "delete", Index: 1},
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// PrevNode
|
||||
{
|
||||
indexHdr: "15",
|
||||
body: `{"action":"delete", "prevNode": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
|
||||
hdr: "15",
|
||||
body: `{"action":"delete", "prevNode": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
|
||||
wantRes: &Response{
|
||||
Action: "delete",
|
||||
Index: 15,
|
||||
@ -707,8 +697,8 @@ func TestUnmarshalSuccessfulResponse(t *testing.T) {
|
||||
|
||||
// Node
|
||||
{
|
||||
indexHdr: "15",
|
||||
body: `{"action":"get", "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10, "ttl": 10, "expiration": "2015-04-07T04:40:23.044979686Z"}}`,
|
||||
hdr: "15",
|
||||
body: `{"action":"get", "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10, "ttl": 10, "expiration": "2015-04-07T04:40:23.044979686Z"}}`,
|
||||
wantRes: &Response{
|
||||
Action: "get",
|
||||
Index: 15,
|
||||
@ -727,9 +717,8 @@ func TestUnmarshalSuccessfulResponse(t *testing.T) {
|
||||
|
||||
// Node Dir
|
||||
{
|
||||
indexHdr: "15",
|
||||
clusterIDHdr: "abcdef",
|
||||
body: `{"action":"get", "node": {"key": "/foo", "dir": true, "modifiedIndex": 12, "createdIndex": 10}}`,
|
||||
hdr: "15",
|
||||
body: `{"action":"get", "node": {"key": "/foo", "dir": true, "modifiedIndex": 12, "createdIndex": 10}}`,
|
||||
wantRes: &Response{
|
||||
Action: "get",
|
||||
Index: 15,
|
||||
@ -739,16 +728,15 @@ func TestUnmarshalSuccessfulResponse(t *testing.T) {
|
||||
ModifiedIndex: 12,
|
||||
CreatedIndex: 10,
|
||||
},
|
||||
PrevNode: nil,
|
||||
ClusterID: "abcdef",
|
||||
PrevNode: nil,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// PrevNode and Node
|
||||
{
|
||||
indexHdr: "15",
|
||||
body: `{"action":"update", "prevNode": {"key": "/foo", "value": "baz", "modifiedIndex": 10, "createdIndex": 10}, "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
|
||||
hdr: "15",
|
||||
body: `{"action":"update", "prevNode": {"key": "/foo", "value": "baz", "modifiedIndex": 10, "createdIndex": 10}, "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
|
||||
wantRes: &Response{
|
||||
Action: "update",
|
||||
Index: 15,
|
||||
@ -770,24 +758,24 @@ func TestUnmarshalSuccessfulResponse(t *testing.T) {
|
||||
|
||||
// Garbage in body
|
||||
{
|
||||
indexHdr: "",
|
||||
body: `garbage`,
|
||||
wantRes: nil,
|
||||
wantErr: true,
|
||||
hdr: "",
|
||||
body: `garbage`,
|
||||
wantRes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// non-integer index
|
||||
{
|
||||
indexHdr: "poo",
|
||||
body: `{}`,
|
||||
wantRes: nil,
|
||||
wantErr: true,
|
||||
hdr: "poo",
|
||||
body: `{}`,
|
||||
wantRes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
h := make(http.Header)
|
||||
h.Add("X-Etcd-Index", tt.indexHdr)
|
||||
h.Add("X-Etcd-Index", tt.hdr)
|
||||
res, err := unmarshalSuccessfulKeysResponse(h, []byte(tt.body))
|
||||
if tt.wantErr != (err != nil) {
|
||||
t.Errorf("#%d: wantErr=%t, err=%v", i, tt.wantErr, err)
|
||||
|
@ -14,20 +14,6 @@
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
roleNotFoundRegExp *regexp.Regexp
|
||||
userNotFoundRegExp *regexp.Regexp
|
||||
)
|
||||
|
||||
func init() {
|
||||
roleNotFoundRegExp = regexp.MustCompile("auth: Role .* does not exist.")
|
||||
userNotFoundRegExp = regexp.MustCompile("auth: User .* does not exist.")
|
||||
}
|
||||
|
||||
// IsKeyNotFound returns true if the error code is ErrorCodeKeyNotFound.
|
||||
func IsKeyNotFound(err error) bool {
|
||||
if cErr, ok := err.(Error); ok {
|
||||
@ -35,19 +21,3 @@ func IsKeyNotFound(err error) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsRoleNotFound returns true if the error means role not found of v2 API.
|
||||
func IsRoleNotFound(err error) bool {
|
||||
if ae, ok := err.(authError); ok {
|
||||
return roleNotFoundRegExp.MatchString(ae.Message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsUserNotFound returns true if the error means user not found of v2 API.
|
||||
func IsUserNotFound(err error) bool {
|
||||
if ae, ok := err.(authError); ok {
|
||||
return userNotFoundRegExp.MatchString(ae.Message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -72,10 +72,6 @@ if err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
## Metrics
|
||||
|
||||
The etcd client optionally exposes RPC metrics through [go-grpc-prometheus](https://github.com/grpc-ecosystem/go-grpc-prometheus). See the [examples](https://github.com/coreos/etcd/blob/master/clientv3/example_metrics_test.go).
|
||||
|
||||
## Examples
|
||||
|
||||
More code examples can be found at [GoDoc](https://godoc.org/github.com/coreos/etcd/clientv3).
|
||||
|
@ -43,7 +43,6 @@ type (
|
||||
AuthRoleListResponse pb.AuthRoleListResponse
|
||||
|
||||
PermissionType authpb.Permission_Type
|
||||
Permission authpb.Permission
|
||||
)
|
||||
|
||||
const (
|
||||
@ -116,12 +115,12 @@ func NewAuth(c *Client) Auth {
|
||||
}
|
||||
|
||||
func (auth *auth) AuthEnable(ctx context.Context) (*AuthEnableResponse, error) {
|
||||
resp, err := auth.remote.AuthEnable(ctx, &pb.AuthEnableRequest{}, grpc.FailFast(false))
|
||||
resp, err := auth.remote.AuthEnable(ctx, &pb.AuthEnableRequest{})
|
||||
return (*AuthEnableResponse)(resp), toErr(ctx, err)
|
||||
}
|
||||
|
||||
func (auth *auth) AuthDisable(ctx context.Context) (*AuthDisableResponse, error) {
|
||||
resp, err := auth.remote.AuthDisable(ctx, &pb.AuthDisableRequest{}, grpc.FailFast(false))
|
||||
resp, err := auth.remote.AuthDisable(ctx, &pb.AuthDisableRequest{})
|
||||
return (*AuthDisableResponse)(resp), toErr(ctx, err)
|
||||
}
|
||||
|
||||
|
@ -21,14 +21,8 @@ import (
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
// ErrNoAddrAvilable is returned by Get() when the balancer does not have
|
||||
// any active connection to endpoints at the time.
|
||||
// This error is returned only when opts.BlockingWait is true.
|
||||
var ErrNoAddrAvilable = grpc.Errorf(codes.Unavailable, "there is no address available")
|
||||
|
||||
// simpleBalancer does the bare minimum to expose multiple eps
|
||||
// to the grpc reconnection code path
|
||||
type simpleBalancer struct {
|
||||
@ -48,11 +42,6 @@ type simpleBalancer struct {
|
||||
// upc closes when upEps transitions from empty to non-zero or the balancer closes.
|
||||
upc chan struct{}
|
||||
|
||||
// grpc issues TLS cert checks using the string passed into dial so
|
||||
// that string must be the host. To recover the full scheme://host URL,
|
||||
// have a map from hosts to the original endpoint.
|
||||
host2ep map[string]string
|
||||
|
||||
// pinAddr is the currently pinned address; set to the empty string on
|
||||
// intialization and shutdown.
|
||||
pinAddr string
|
||||
@ -73,12 +62,11 @@ func newSimpleBalancer(eps []string) *simpleBalancer {
|
||||
readyc: make(chan struct{}),
|
||||
upEps: make(map[string]struct{}),
|
||||
upc: make(chan struct{}),
|
||||
host2ep: getHost2ep(eps),
|
||||
}
|
||||
return sb
|
||||
}
|
||||
|
||||
func (b *simpleBalancer) Start(target string, config grpc.BalancerConfig) error { return nil }
|
||||
func (b *simpleBalancer) Start(target string) error { return nil }
|
||||
|
||||
func (b *simpleBalancer) ConnectNotify() <-chan struct{} {
|
||||
b.mu.Lock()
|
||||
@ -86,49 +74,6 @@ func (b *simpleBalancer) ConnectNotify() <-chan struct{} {
|
||||
return b.upc
|
||||
}
|
||||
|
||||
func (b *simpleBalancer) getEndpoint(host string) string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.host2ep[host]
|
||||
}
|
||||
|
||||
func getHost2ep(eps []string) map[string]string {
|
||||
hm := make(map[string]string, len(eps))
|
||||
for i := range eps {
|
||||
_, host, _ := parseEndpoint(eps[i])
|
||||
hm[host] = eps[i]
|
||||
}
|
||||
return hm
|
||||
}
|
||||
|
||||
func (b *simpleBalancer) updateAddrs(eps []string) {
|
||||
np := getHost2ep(eps)
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
match := len(np) == len(b.host2ep)
|
||||
for k, v := range np {
|
||||
if b.host2ep[k] != v {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
// same endpoints, so no need to update address
|
||||
return
|
||||
}
|
||||
|
||||
b.host2ep = np
|
||||
|
||||
addrs := make([]grpc.Address, 0, len(eps))
|
||||
for i := range eps {
|
||||
addrs = append(addrs, grpc.Address{Addr: getHost(eps[i])})
|
||||
}
|
||||
b.addrs = addrs
|
||||
b.notifyCh <- addrs
|
||||
}
|
||||
|
||||
func (b *simpleBalancer) Up(addr grpc.Address) func(error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
@ -168,25 +113,6 @@ func (b *simpleBalancer) Up(addr grpc.Address) func(error) {
|
||||
|
||||
func (b *simpleBalancer) Get(ctx context.Context, opts grpc.BalancerGetOptions) (grpc.Address, func(), error) {
|
||||
var addr string
|
||||
|
||||
// If opts.BlockingWait is false (for fail-fast RPCs), it should return
|
||||
// an address it has notified via Notify immediately instead of blocking.
|
||||
if !opts.BlockingWait {
|
||||
b.mu.RLock()
|
||||
closed := b.closed
|
||||
addr = b.pinAddr
|
||||
upEps := len(b.upEps)
|
||||
b.mu.RUnlock()
|
||||
if closed {
|
||||
return grpc.Address{Addr: ""}, nil, grpc.ErrClientConnClosing
|
||||
}
|
||||
|
||||
if upEps == 0 {
|
||||
return grpc.Address{Addr: ""}, nil, ErrNoAddrAvilable
|
||||
}
|
||||
return grpc.Address{Addr: addr}, func() {}, nil
|
||||
}
|
||||
|
||||
for {
|
||||
b.mu.RLock()
|
||||
ch := b.upc
|
||||
|
@ -1,106 +0,0 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clientv3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
endpoints = []string{"localhost:2379", "localhost:22379", "localhost:32379"}
|
||||
)
|
||||
|
||||
func TestBalancerGetUnblocking(t *testing.T) {
|
||||
sb := newSimpleBalancer(endpoints)
|
||||
unblockingOpts := grpc.BalancerGetOptions{BlockingWait: false}
|
||||
|
||||
_, _, err := sb.Get(context.Background(), unblockingOpts)
|
||||
if err != ErrNoAddrAvilable {
|
||||
t.Errorf("Get() with no up endpoints should return ErrNoAddrAvailable, got: %v", err)
|
||||
}
|
||||
|
||||
down1 := sb.Up(grpc.Address{Addr: endpoints[1]})
|
||||
down2 := sb.Up(grpc.Address{Addr: endpoints[2]})
|
||||
addrFirst, putFun, err := sb.Get(context.Background(), unblockingOpts)
|
||||
if err != nil {
|
||||
t.Errorf("Get() with up endpoints should success, got %v", err)
|
||||
}
|
||||
if addrFirst.Addr != endpoints[1] {
|
||||
t.Errorf("Get() didn't return expected address, got %v", addrFirst)
|
||||
}
|
||||
if putFun == nil {
|
||||
t.Errorf("Get() returned unexpected nil put function")
|
||||
}
|
||||
addrSecond, _, _ := sb.Get(context.Background(), unblockingOpts)
|
||||
if addrFirst.Addr != addrSecond.Addr {
|
||||
t.Errorf("Get() didn't return the same address as previous call, got %v and %v", addrFirst, addrSecond)
|
||||
}
|
||||
|
||||
down1(errors.New("error"))
|
||||
down2(errors.New("error"))
|
||||
_, _, err = sb.Get(context.Background(), unblockingOpts)
|
||||
if err != ErrNoAddrAvilable {
|
||||
t.Errorf("Get() with no up endpoints should return ErrNoAddrAvailable, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBalancerGetBlocking(t *testing.T) {
|
||||
sb := newSimpleBalancer(endpoints)
|
||||
blockingOpts := grpc.BalancerGetOptions{BlockingWait: true}
|
||||
|
||||
ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*100)
|
||||
_, _, err := sb.Get(ctx, blockingOpts)
|
||||
if err != context.DeadlineExceeded {
|
||||
t.Errorf("Get() with no up endpoints should timeout, got %v", err)
|
||||
}
|
||||
|
||||
downC := make(chan func(error), 1)
|
||||
|
||||
go func() {
|
||||
// ensure sb.Up() will be called after sb.Get() to see if Up() releases blocking Get()
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
downC <- sb.Up(grpc.Address{Addr: endpoints[1]})
|
||||
}()
|
||||
addrFirst, putFun, err := sb.Get(context.Background(), blockingOpts)
|
||||
if err != nil {
|
||||
t.Errorf("Get() with up endpoints should success, got %v", err)
|
||||
}
|
||||
if addrFirst.Addr != endpoints[1] {
|
||||
t.Errorf("Get() didn't return expected address, got %v", addrFirst)
|
||||
}
|
||||
if putFun == nil {
|
||||
t.Errorf("Get() returned unexpected nil put function")
|
||||
}
|
||||
down1 := <-downC
|
||||
|
||||
down2 := sb.Up(grpc.Address{Addr: endpoints[2]})
|
||||
addrSecond, _, _ := sb.Get(context.Background(), blockingOpts)
|
||||
if addrFirst.Addr != addrSecond.Addr {
|
||||
t.Errorf("Get() didn't return the same address as previous call, got %v and %v", addrFirst, addrSecond)
|
||||
}
|
||||
|
||||
down1(errors.New("error"))
|
||||
down2(errors.New("error"))
|
||||
ctx, _ = context.WithTimeout(context.Background(), time.Millisecond*100)
|
||||
_, _, err = sb.Get(ctx, blockingOpts)
|
||||
if err != context.DeadlineExceeded {
|
||||
t.Errorf("Get() with no up endpoints should timeout, got %v", err)
|
||||
}
|
||||
}
|
@ -18,18 +18,17 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
@ -47,12 +46,11 @@ type Client struct {
|
||||
Auth
|
||||
Maintenance
|
||||
|
||||
conn *grpc.ClientConn
|
||||
cfg Config
|
||||
creds *credentials.TransportCredentials
|
||||
balancer *simpleBalancer
|
||||
retryWrapper retryRpcFunc
|
||||
retryAuthWrapper retryRpcFunc
|
||||
conn *grpc.ClientConn
|
||||
cfg Config
|
||||
creds *credentials.TransportCredentials
|
||||
balancer *simpleBalancer
|
||||
retryWrapper retryRpcFunc
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@ -61,8 +59,6 @@ type Client struct {
|
||||
Username string
|
||||
// Password is a password for authentication
|
||||
Password string
|
||||
// tokenCred is an instance of WithPerRPCCredentials()'s argument
|
||||
tokenCred *authTokenCredential
|
||||
}
|
||||
|
||||
// New creates a new etcdv3 client from a given configuration.
|
||||
@ -91,8 +87,6 @@ func NewFromConfigFile(path string) (*Client, error) {
|
||||
// Close shuts down the client's etcd connections.
|
||||
func (c *Client) Close() error {
|
||||
c.cancel()
|
||||
c.Watcher.Close()
|
||||
c.Lease.Close()
|
||||
return toErr(c.ctx, c.conn.Close())
|
||||
}
|
||||
|
||||
@ -102,54 +96,10 @@ func (c *Client) Close() error {
|
||||
func (c *Client) Ctx() context.Context { return c.ctx }
|
||||
|
||||
// Endpoints lists the registered endpoints for the client.
|
||||
func (c *Client) Endpoints() (eps []string) {
|
||||
// copy the slice; protect original endpoints from being changed
|
||||
eps = make([]string, len(c.cfg.Endpoints))
|
||||
copy(eps, c.cfg.Endpoints)
|
||||
return
|
||||
}
|
||||
|
||||
// SetEndpoints updates client's endpoints.
|
||||
func (c *Client) SetEndpoints(eps ...string) {
|
||||
c.cfg.Endpoints = eps
|
||||
c.balancer.updateAddrs(eps)
|
||||
}
|
||||
|
||||
// Sync synchronizes client's endpoints with the known endpoints from the etcd membership.
|
||||
func (c *Client) Sync(ctx context.Context) error {
|
||||
mresp, err := c.MemberList(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var eps []string
|
||||
for _, m := range mresp.Members {
|
||||
eps = append(eps, m.ClientURLs...)
|
||||
}
|
||||
c.SetEndpoints(eps...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) autoSync() {
|
||||
if c.cfg.AutoSyncInterval == time.Duration(0) {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
case <-time.After(c.cfg.AutoSyncInterval):
|
||||
ctx, _ := context.WithTimeout(c.ctx, 5*time.Second)
|
||||
if err := c.Sync(ctx); err != nil && err != c.ctx.Err() {
|
||||
logger.Println("Auto sync endpoints failed:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func (c *Client) Endpoints() []string { return c.cfg.Endpoints }
|
||||
|
||||
type authTokenCredential struct {
|
||||
token string
|
||||
tokenMu *sync.RWMutex
|
||||
token string
|
||||
}
|
||||
|
||||
func (cred authTokenCredential) RequireTransportSecurity() bool {
|
||||
@ -157,38 +107,24 @@ func (cred authTokenCredential) RequireTransportSecurity() bool {
|
||||
}
|
||||
|
||||
func (cred authTokenCredential) GetRequestMetadata(ctx context.Context, s ...string) (map[string]string, error) {
|
||||
cred.tokenMu.RLock()
|
||||
defer cred.tokenMu.RUnlock()
|
||||
return map[string]string{
|
||||
"token": cred.token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseEndpoint(endpoint string) (proto string, host string, scheme string) {
|
||||
func (c *Client) dialTarget(endpoint string) (proto string, host string, creds *credentials.TransportCredentials) {
|
||||
proto = "tcp"
|
||||
host = endpoint
|
||||
creds = c.creds
|
||||
url, uerr := url.Parse(endpoint)
|
||||
if uerr != nil || !strings.Contains(endpoint, "://") {
|
||||
return
|
||||
}
|
||||
scheme = url.Scheme
|
||||
|
||||
// strip scheme:// prefix since grpc dials by host
|
||||
host = url.Host
|
||||
switch url.Scheme {
|
||||
case "http", "https":
|
||||
case "unix":
|
||||
proto = "unix"
|
||||
default:
|
||||
proto, host = "", ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) processCreds(scheme string) (creds *credentials.TransportCredentials) {
|
||||
creds = c.creds
|
||||
switch scheme {
|
||||
case "unix":
|
||||
case "http":
|
||||
creds = nil
|
||||
case "https":
|
||||
@ -199,7 +135,7 @@ func (c *Client) processCreds(scheme string) (creds *credentials.TransportCreden
|
||||
emptyCreds := credentials.NewTLS(tlsconfig)
|
||||
creds = &emptyCreds
|
||||
default:
|
||||
creds = nil
|
||||
return "", "", nil
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -211,8 +147,17 @@ func (c *Client) dialSetupOpts(endpoint string, dopts ...grpc.DialOption) (opts
|
||||
}
|
||||
opts = append(opts, dopts...)
|
||||
|
||||
// grpc issues TLS cert checks using the string passed into dial so
|
||||
// that string must be the host. To recover the full scheme://host URL,
|
||||
// have a map from hosts to the original endpoint.
|
||||
host2ep := make(map[string]string)
|
||||
for i := range c.cfg.Endpoints {
|
||||
_, host, _ := c.dialTarget(c.cfg.Endpoints[i])
|
||||
host2ep[host] = c.cfg.Endpoints[i]
|
||||
}
|
||||
|
||||
f := func(host string, t time.Duration) (net.Conn, error) {
|
||||
proto, host, _ := parseEndpoint(c.balancer.getEndpoint(host))
|
||||
proto, host, _ := c.dialTarget(host2ep[host])
|
||||
if proto == "" {
|
||||
return nil, fmt.Errorf("unknown scheme for %q", host)
|
||||
}
|
||||
@ -221,15 +166,11 @@ func (c *Client) dialSetupOpts(endpoint string, dopts ...grpc.DialOption) (opts
|
||||
return nil, c.ctx.Err()
|
||||
default:
|
||||
}
|
||||
dialer := &net.Dialer{Timeout: t}
|
||||
return dialer.DialContext(c.ctx, proto, host)
|
||||
return net.DialTimeout(proto, host, t)
|
||||
}
|
||||
opts = append(opts, grpc.WithDialer(f))
|
||||
|
||||
creds := c.creds
|
||||
if _, _, scheme := parseEndpoint(endpoint); len(scheme) != 0 {
|
||||
creds = c.processCreds(scheme)
|
||||
}
|
||||
_, _, creds := c.dialTarget(endpoint)
|
||||
if creds != nil {
|
||||
opts = append(opts, grpc.WithTransportCredentials(*creds))
|
||||
} else {
|
||||
@ -244,56 +185,24 @@ func (c *Client) Dial(endpoint string) (*grpc.ClientConn, error) {
|
||||
return c.dial(endpoint)
|
||||
}
|
||||
|
||||
func (c *Client) getToken(ctx context.Context) error {
|
||||
var err error // return last error in a case of fail
|
||||
var auth *authenticator
|
||||
|
||||
for i := 0; i < len(c.cfg.Endpoints); i++ {
|
||||
endpoint := c.cfg.Endpoints[i]
|
||||
host := getHost(endpoint)
|
||||
// use dial options without dopts to avoid reusing the client balancer
|
||||
auth, err = newAuthenticator(host, c.dialSetupOpts(endpoint))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer auth.close()
|
||||
|
||||
var resp *AuthenticateResponse
|
||||
resp, err = auth.authenticate(ctx, c.Username, c.Password)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
c.tokenCred.tokenMu.Lock()
|
||||
c.tokenCred.token = resp.Token
|
||||
c.tokenCred.tokenMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) dial(endpoint string, dopts ...grpc.DialOption) (*grpc.ClientConn, error) {
|
||||
opts := c.dialSetupOpts(endpoint, dopts...)
|
||||
host := getHost(endpoint)
|
||||
if c.Username != "" && c.Password != "" {
|
||||
c.tokenCred = &authTokenCredential{
|
||||
tokenMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
err := c.getToken(context.TODO())
|
||||
// use dial options without dopts to avoid reusing the client balancer
|
||||
auth, err := newAuthenticator(host, c.dialSetupOpts(endpoint))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer auth.close()
|
||||
|
||||
opts = append(opts, grpc.WithPerRPCCredentials(c.tokenCred))
|
||||
resp, err := auth.authenticate(c.ctx, c.Username, c.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts = append(opts, grpc.WithPerRPCCredentials(authTokenCredential{token: resp.Token}))
|
||||
}
|
||||
|
||||
// add metrics options
|
||||
opts = append(opts, grpc.WithUnaryInterceptor(prometheus.UnaryClientInterceptor))
|
||||
opts = append(opts, grpc.WithStreamInterceptor(prometheus.StreamClientInterceptor))
|
||||
|
||||
conn, err := grpc.Dial(host, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -339,7 +248,6 @@ func newClient(cfg *Config) (*Client, error) {
|
||||
}
|
||||
client.conn = conn
|
||||
client.retryWrapper = client.newRetryWrapper()
|
||||
client.retryAuthWrapper = client.newAuthRetryWrapper()
|
||||
|
||||
// wait for a connection
|
||||
if cfg.DialTimeout > 0 {
|
||||
@ -364,8 +272,13 @@ func newClient(cfg *Config) (*Client, error) {
|
||||
client.Watcher = NewWatcher(client)
|
||||
client.Auth = NewAuth(client)
|
||||
client.Maintenance = NewMaintenance(client)
|
||||
if cfg.Logger != nil {
|
||||
logger.Set(cfg.Logger)
|
||||
} else {
|
||||
// disable client side grpc by default
|
||||
logger.Set(log.New(ioutil.Discard, "", 0))
|
||||
}
|
||||
|
||||
go client.autoSync()
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@ -381,14 +294,17 @@ func isHaltErr(ctx context.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
code := grpc.Code(err)
|
||||
// Unavailable codes mean the system will be right back.
|
||||
// (e.g., can't connect, lost leader)
|
||||
// Treat Internal codes as if something failed, leaving the
|
||||
// system in an inconsistent state, but retrying could make progress.
|
||||
// (e.g., failed in middle of send, corrupted frame)
|
||||
// TODO: are permanent Internal errors possible from grpc?
|
||||
return code != codes.Unavailable && code != codes.Internal
|
||||
eErr := rpctypes.Error(err)
|
||||
if _, ok := eErr.(rpctypes.EtcdError); ok {
|
||||
return eErr != rpctypes.ErrStopped && eErr != rpctypes.ErrNoLeader
|
||||
}
|
||||
// treat etcdserver errors not recognized by the client as halting
|
||||
return isConnClosing(err) || strings.Contains(err.Error(), "etcdserver:")
|
||||
}
|
||||
|
||||
// isConnClosing returns true if the error matches a grpc client closing error
|
||||
func isConnClosing(err error) bool {
|
||||
return strings.Contains(err.Error(), grpc.ErrClientConnClosing.Error())
|
||||
}
|
||||
|
||||
func toErr(ctx context.Context, err error) error {
|
||||
@ -396,20 +312,12 @@ func toErr(ctx context.Context, err error) error {
|
||||
return nil
|
||||
}
|
||||
err = rpctypes.Error(err)
|
||||
if _, ok := err.(rpctypes.EtcdError); ok {
|
||||
return err
|
||||
}
|
||||
code := grpc.Code(err)
|
||||
switch code {
|
||||
case codes.DeadlineExceeded:
|
||||
fallthrough
|
||||
case codes.Canceled:
|
||||
if ctx.Err() != nil {
|
||||
err = ctx.Err()
|
||||
}
|
||||
case codes.Unavailable:
|
||||
switch {
|
||||
case ctx.Err() != nil && strings.Contains(err.Error(), "context"):
|
||||
err = ctx.Err()
|
||||
case strings.Contains(err.Error(), ErrNoAvailableEndpoints.Error()):
|
||||
err = ErrNoAvailableEndpoints
|
||||
case codes.FailedPrecondition:
|
||||
case strings.Contains(err.Error(), grpc.ErrClientConnClosing.Error()):
|
||||
err = grpc.ErrClientConnClosing
|
||||
}
|
||||
return err
|
||||
|
@ -16,57 +16,15 @@ package clientv3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestDialCancel(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
// accept first connection so client is created with dial timeout
|
||||
ln, err := net.Listen("unix", "dialcancel:12345")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
ep := "unix://dialcancel:12345"
|
||||
cfg := Config{
|
||||
Endpoints: []string{ep},
|
||||
DialTimeout: 30 * time.Second}
|
||||
c, err := New(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// connect to ipv4 blackhole so dial blocks
|
||||
c.SetEndpoints("http://254.0.0.1:12345")
|
||||
|
||||
// issue Get to force redial attempts
|
||||
go c.Get(context.TODO(), "abc")
|
||||
|
||||
// wait a little bit so client close is after dial starts
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
donec := make(chan struct{})
|
||||
go func() {
|
||||
defer close(donec)
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("failed to close")
|
||||
case <-donec:
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialTimeout(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
@ -114,11 +72,11 @@ func TestIsHaltErr(t *testing.T) {
|
||||
if !isHaltErr(nil, fmt.Errorf("etcdserver: some etcdserver error")) {
|
||||
t.Errorf(`error prefixed with "etcdserver: " should be Halted by default`)
|
||||
}
|
||||
if isHaltErr(nil, rpctypes.ErrGRPCStopped) {
|
||||
t.Errorf("error %v should not halt", rpctypes.ErrGRPCStopped)
|
||||
if isHaltErr(nil, etcdserver.ErrStopped) {
|
||||
t.Errorf("error %v should not halt", etcdserver.ErrStopped)
|
||||
}
|
||||
if isHaltErr(nil, rpctypes.ErrGRPCNoLeader) {
|
||||
t.Errorf("error %v should not halt", rpctypes.ErrGRPCNoLeader)
|
||||
if isHaltErr(nil, etcdserver.ErrNoLeader) {
|
||||
t.Errorf("error %v should not halt", etcdserver.ErrNoLeader)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
if isHaltErr(ctx, nil) {
|
||||
|
@ -78,7 +78,7 @@ func (c *cluster) MemberUpdate(ctx context.Context, id uint64, peerAddrs []strin
|
||||
// it is safe to retry on update.
|
||||
for {
|
||||
r := &pb.MemberUpdateRequest{ID: id, PeerURLs: peerAddrs}
|
||||
resp, err := c.remote.MemberUpdate(ctx, r, grpc.FailFast(false))
|
||||
resp, err := c.remote.MemberUpdate(ctx, r)
|
||||
if err == nil {
|
||||
return (*MemberUpdateResponse)(resp), nil
|
||||
}
|
||||
|
@ -36,8 +36,6 @@ func Compare(cmp Cmp, result string, v interface{}) Cmp {
|
||||
switch result {
|
||||
case "=":
|
||||
r = pb.Compare_EQUAL
|
||||
case "!=":
|
||||
r = pb.Compare_NOT_EQUAL
|
||||
case ">":
|
||||
r = pb.Compare_GREATER
|
||||
case "<":
|
||||
|
@ -29,7 +29,7 @@ var (
|
||||
)
|
||||
|
||||
type Election struct {
|
||||
session *Session
|
||||
client *v3.Client
|
||||
|
||||
keyPrefix string
|
||||
|
||||
@ -39,18 +39,20 @@ type Election struct {
|
||||
}
|
||||
|
||||
// NewElection returns a new election on a given key prefix.
|
||||
func NewElection(s *Session, pfx string) *Election {
|
||||
return &Election{session: s, keyPrefix: pfx + "/"}
|
||||
func NewElection(client *v3.Client, pfx string) *Election {
|
||||
return &Election{client: client, keyPrefix: pfx + "/"}
|
||||
}
|
||||
|
||||
// Campaign puts a value as eligible for the election. It blocks until
|
||||
// it is elected, an error occurs, or the context is cancelled.
|
||||
func (e *Election) Campaign(ctx context.Context, val string) error {
|
||||
s := e.session
|
||||
client := e.session.Client()
|
||||
s, serr := NewSession(e.client)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
|
||||
k := fmt.Sprintf("%s%x", e.keyPrefix, s.Lease())
|
||||
txn := client.Txn(ctx).If(v3.Compare(v3.CreateRevision(k), "=", 0))
|
||||
k := fmt.Sprintf("%s/%x", e.keyPrefix, s.Lease())
|
||||
txn := e.client.Txn(ctx).If(v3.Compare(v3.CreateRevision(k), "=", 0))
|
||||
txn = txn.Then(v3.OpPut(k, val, v3.WithLease(s.Lease())))
|
||||
txn = txn.Else(v3.OpGet(k))
|
||||
resp, err := txn.Commit()
|
||||
@ -69,12 +71,12 @@ func (e *Election) Campaign(ctx context.Context, val string) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = waitDeletes(ctx, client, e.keyPrefix, e.leaderRev-1)
|
||||
err = waitDeletes(ctx, e.client, e.keyPrefix, v3.WithPrefix(), v3.WithRev(e.leaderRev-1))
|
||||
if err != nil {
|
||||
// clean up in case of context cancel
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
e.Resign(client.Ctx())
|
||||
e.Resign(e.client.Ctx())
|
||||
default:
|
||||
e.leaderSession = nil
|
||||
}
|
||||
@ -89,9 +91,8 @@ func (e *Election) Proclaim(ctx context.Context, val string) error {
|
||||
if e.leaderSession == nil {
|
||||
return ErrElectionNotLeader
|
||||
}
|
||||
client := e.session.Client()
|
||||
cmp := v3.Compare(v3.CreateRevision(e.leaderKey), "=", e.leaderRev)
|
||||
txn := client.Txn(ctx).If(cmp)
|
||||
txn := e.client.Txn(ctx).If(cmp)
|
||||
txn = txn.Then(v3.OpPut(e.leaderKey, val, v3.WithLease(e.leaderSession.Lease())))
|
||||
tresp, terr := txn.Commit()
|
||||
if terr != nil {
|
||||
@ -109,8 +110,7 @@ func (e *Election) Resign(ctx context.Context) (err error) {
|
||||
if e.leaderSession == nil {
|
||||
return nil
|
||||
}
|
||||
client := e.session.Client()
|
||||
_, err = client.Delete(ctx, e.leaderKey)
|
||||
_, err = e.client.Delete(ctx, e.leaderKey)
|
||||
e.leaderKey = ""
|
||||
e.leaderSession = nil
|
||||
return err
|
||||
@ -118,8 +118,7 @@ func (e *Election) Resign(ctx context.Context) (err error) {
|
||||
|
||||
// Leader returns the leader value for the current election.
|
||||
func (e *Election) Leader(ctx context.Context) (string, error) {
|
||||
client := e.session.Client()
|
||||
resp, err := client.Get(ctx, e.keyPrefix, v3.WithFirstCreate()...)
|
||||
resp, err := e.client.Get(ctx, e.keyPrefix, v3.WithFirstCreate()...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if len(resp.Kvs) == 0 {
|
||||
@ -139,11 +138,9 @@ func (e *Election) Observe(ctx context.Context) <-chan v3.GetResponse {
|
||||
}
|
||||
|
||||
func (e *Election) observe(ctx context.Context, ch chan<- v3.GetResponse) {
|
||||
client := e.session.Client()
|
||||
|
||||
defer close(ch)
|
||||
for {
|
||||
resp, err := client.Get(ctx, e.keyPrefix, v3.WithFirstCreate()...)
|
||||
resp, err := e.client.Get(ctx, e.keyPrefix, v3.WithFirstCreate()...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -154,7 +151,7 @@ func (e *Election) observe(ctx context.Context, ch chan<- v3.GetResponse) {
|
||||
if len(resp.Kvs) == 0 {
|
||||
// wait for first key put on prefix
|
||||
opts := []v3.OpOption{v3.WithRev(resp.Header.Revision), v3.WithPrefix()}
|
||||
wch := client.Watch(cctx, e.keyPrefix, opts...)
|
||||
wch := e.client.Watch(cctx, e.keyPrefix, opts...)
|
||||
|
||||
for kv == nil {
|
||||
wr, ok := <-wch
|
||||
@ -174,7 +171,7 @@ func (e *Election) observe(ctx context.Context, ch chan<- v3.GetResponse) {
|
||||
kv = resp.Kvs[0]
|
||||
}
|
||||
|
||||
wch := client.Watch(cctx, string(kv.Key), v3.WithRev(kv.ModRevision))
|
||||
wch := e.client.Watch(cctx, string(kv.Key), v3.WithRev(kv.ModRevision))
|
||||
keyDeleted := false
|
||||
for !keyDeleted {
|
||||
wr, ok := <-wch
|
||||
|
@ -16,6 +16,7 @@ package concurrency
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
@ -25,40 +26,46 @@ import (
|
||||
func waitDelete(ctx context.Context, client *v3.Client, key string, rev int64) error {
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var wr v3.WatchResponse
|
||||
wch := client.Watch(cctx, key, v3.WithRev(rev))
|
||||
for wr = range wch {
|
||||
for wr := range wch {
|
||||
for _, ev := range wr.Events {
|
||||
if ev.Type == mvccpb.DELETE {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := wr.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("lost watcher waiting for delete")
|
||||
}
|
||||
|
||||
// waitDeletes efficiently waits until all keys matching the prefix and no greater
|
||||
// than the create revision.
|
||||
func waitDeletes(ctx context.Context, client *v3.Client, pfx string, maxCreateRev int64) error {
|
||||
getOpts := append(v3.WithLastCreate(), v3.WithMaxCreateRev(maxCreateRev))
|
||||
for {
|
||||
resp, err := client.Get(ctx, pfx, getOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
// waitDeletes efficiently waits until all keys matched by Get(key, opts...) are deleted
|
||||
func waitDeletes(ctx context.Context, client *v3.Client, key string, opts ...v3.OpOption) error {
|
||||
getOpts := []v3.OpOption{v3.WithSort(v3.SortByCreateRevision, v3.SortAscend)}
|
||||
getOpts = append(getOpts, opts...)
|
||||
resp, err := client.Get(ctx, key, getOpts...)
|
||||
maxRev := int64(math.MaxInt64)
|
||||
getOpts = append(getOpts, v3.WithRev(0))
|
||||
for err == nil {
|
||||
for len(resp.Kvs) > 0 {
|
||||
i := len(resp.Kvs) - 1
|
||||
if resp.Kvs[i].CreateRevision <= maxRev {
|
||||
break
|
||||
}
|
||||
resp.Kvs = resp.Kvs[:i]
|
||||
}
|
||||
if len(resp.Kvs) == 0 {
|
||||
return nil
|
||||
break
|
||||
}
|
||||
lastKey := string(resp.Kvs[0].Key)
|
||||
if err = waitDelete(ctx, client, lastKey, resp.Header.Revision); err != nil {
|
||||
return err
|
||||
lastKV := resp.Kvs[len(resp.Kvs)-1]
|
||||
maxRev = lastKV.CreateRevision
|
||||
err = waitDelete(ctx, client, string(lastKV.Key), maxRev)
|
||||
if err != nil || len(resp.Kvs) == 1 {
|
||||
break
|
||||
}
|
||||
getOpts = append(getOpts, v3.WithLimit(int64(len(resp.Kvs)-1)))
|
||||
resp, err = client.Get(ctx, key, getOpts...)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -24,22 +24,24 @@ import (
|
||||
|
||||
// Mutex implements the sync Locker interface with etcd
|
||||
type Mutex struct {
|
||||
s *Session
|
||||
client *v3.Client
|
||||
|
||||
pfx string
|
||||
myKey string
|
||||
myRev int64
|
||||
}
|
||||
|
||||
func NewMutex(s *Session, pfx string) *Mutex {
|
||||
return &Mutex{s, pfx + "/", "", -1}
|
||||
func NewMutex(client *v3.Client, pfx string) *Mutex {
|
||||
return &Mutex{client, pfx + "/", "", -1}
|
||||
}
|
||||
|
||||
// Lock locks the mutex with a cancellable context. If the context is cancelled
|
||||
// while trying to acquire the lock, the mutex tries to clean its stale lock entry.
|
||||
func (m *Mutex) Lock(ctx context.Context) error {
|
||||
s := m.s
|
||||
client := m.s.Client()
|
||||
s, serr := NewSession(m.client)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
|
||||
m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease())
|
||||
cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0)
|
||||
@ -47,7 +49,7 @@ func (m *Mutex) Lock(ctx context.Context) error {
|
||||
put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease()))
|
||||
// reuse key in case this session already holds the lock
|
||||
get := v3.OpGet(m.myKey)
|
||||
resp, err := client.Txn(ctx).If(cmp).Then(put).Else(get).Commit()
|
||||
resp, err := m.client.Txn(ctx).If(cmp).Then(put).Else(get).Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -57,19 +59,18 @@ func (m *Mutex) Lock(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// wait for deletion revisions prior to myKey
|
||||
err = waitDeletes(ctx, client, m.pfx, m.myRev-1)
|
||||
err = waitDeletes(ctx, m.client, m.pfx, v3.WithPrefix(), v3.WithRev(m.myRev-1))
|
||||
// release lock key if cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
m.Unlock(client.Ctx())
|
||||
m.Unlock(m.client.Ctx())
|
||||
default:
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Mutex) Unlock(ctx context.Context) error {
|
||||
client := m.s.Client()
|
||||
if _, err := client.Delete(ctx, m.myKey); err != nil {
|
||||
if _, err := m.client.Delete(ctx, m.myKey); err != nil {
|
||||
return err
|
||||
}
|
||||
m.myKey = "\x00"
|
||||
@ -86,19 +87,17 @@ func (m *Mutex) Key() string { return m.myKey }
|
||||
type lockerMutex struct{ *Mutex }
|
||||
|
||||
func (lm *lockerMutex) Lock() {
|
||||
client := lm.s.Client()
|
||||
if err := lm.Mutex.Lock(client.Ctx()); err != nil {
|
||||
if err := lm.Mutex.Lock(lm.client.Ctx()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
func (lm *lockerMutex) Unlock() {
|
||||
client := lm.s.Client()
|
||||
if err := lm.Mutex.Unlock(client.Ctx()); err != nil {
|
||||
if err := lm.Mutex.Unlock(lm.client.Ctx()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewLocker creates a sync.Locker backed by an etcd mutex.
|
||||
func NewLocker(s *Session, pfx string) sync.Locker {
|
||||
return &lockerMutex{NewMutex(s, pfx)}
|
||||
func NewLocker(client *v3.Client, pfx string) sync.Locker {
|
||||
return &lockerMutex{NewMutex(client, pfx)}
|
||||
}
|
||||
|
@ -15,19 +15,26 @@
|
||||
package concurrency
|
||||
|
||||
import (
|
||||
"time"
|
||||
"sync"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const defaultSessionTTL = 60
|
||||
// only keep one ephemeral lease per client
|
||||
var clientSessions clientSessionMgr = clientSessionMgr{sessions: make(map[*v3.Client]*Session)}
|
||||
|
||||
const sessionTTL = 60
|
||||
|
||||
type clientSessionMgr struct {
|
||||
sessions map[*v3.Client]*Session
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Session represents a lease kept alive for the lifetime of a client.
|
||||
// Fault-tolerant applications may use sessions to reason about liveness.
|
||||
type Session struct {
|
||||
client *v3.Client
|
||||
opts *sessionOptions
|
||||
id v3.LeaseID
|
||||
|
||||
cancel context.CancelFunc
|
||||
@ -35,30 +42,37 @@ type Session struct {
|
||||
}
|
||||
|
||||
// NewSession gets the leased session for a client.
|
||||
func NewSession(client *v3.Client, opts ...SessionOption) (*Session, error) {
|
||||
ops := &sessionOptions{ttl: defaultSessionTTL, ctx: client.Ctx()}
|
||||
for _, opt := range opts {
|
||||
opt(ops)
|
||||
func NewSession(client *v3.Client) (*Session, error) {
|
||||
clientSessions.mu.Lock()
|
||||
defer clientSessions.mu.Unlock()
|
||||
if s, ok := clientSessions.sessions[client]; ok {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
resp, err := client.Grant(ops.ctx, int64(ops.ttl))
|
||||
resp, err := client.Grant(client.Ctx(), sessionTTL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := v3.LeaseID(resp.ID)
|
||||
|
||||
ctx, cancel := context.WithCancel(ops.ctx)
|
||||
ctx, cancel := context.WithCancel(client.Ctx())
|
||||
keepAlive, err := client.KeepAlive(ctx, id)
|
||||
if err != nil || keepAlive == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
donec := make(chan struct{})
|
||||
s := &Session{client: client, opts: ops, id: id, cancel: cancel, donec: donec}
|
||||
s := &Session{client: client, id: id, cancel: cancel, donec: donec}
|
||||
clientSessions.sessions[client] = s
|
||||
|
||||
// keep the lease alive until client error or cancelled context
|
||||
go func() {
|
||||
defer close(donec)
|
||||
defer func() {
|
||||
clientSessions.mu.Lock()
|
||||
delete(clientSessions.sessions, client)
|
||||
clientSessions.mu.Unlock()
|
||||
close(donec)
|
||||
}()
|
||||
for range keepAlive {
|
||||
// eat messages until keep alive channel closes
|
||||
}
|
||||
@ -67,11 +81,6 @@ func NewSession(client *v3.Client, opts ...SessionOption) (*Session, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Client is the etcd client that is attached to the session.
|
||||
func (s *Session) Client() *v3.Client {
|
||||
return s.client
|
||||
}
|
||||
|
||||
// Lease is the lease ID for keys bound to the session.
|
||||
func (s *Session) Lease() v3.LeaseID { return s.id }
|
||||
|
||||
@ -90,38 +99,6 @@ func (s *Session) Orphan() {
|
||||
// Close orphans the session and revokes the session lease.
|
||||
func (s *Session) Close() error {
|
||||
s.Orphan()
|
||||
// if revoke takes longer than the ttl, lease is expired anyway
|
||||
ctx, cancel := context.WithTimeout(s.opts.ctx, time.Duration(s.opts.ttl)*time.Second)
|
||||
_, err := s.client.Revoke(ctx, s.id)
|
||||
cancel()
|
||||
_, err := s.client.Revoke(s.client.Ctx(), s.id)
|
||||
return err
|
||||
}
|
||||
|
||||
type sessionOptions struct {
|
||||
ttl int
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// SessionOption configures Session.
|
||||
type SessionOption func(*sessionOptions)
|
||||
|
||||
// WithTTL configures the session's TTL in seconds.
|
||||
// If TTL is <= 0, the default 60 seconds TTL will be used.
|
||||
func WithTTL(ttl int) SessionOption {
|
||||
return func(so *sessionOptions) {
|
||||
if ttl > 0 {
|
||||
so.ttl = ttl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext assigns a context to the session instead of defaulting to
|
||||
// using the client context. This is useful for canceling NewSession and
|
||||
// Close operations immediately without having to close the client. If the
|
||||
// context is canceled before Close() completes, the session's lease will be
|
||||
// abandoned and left to expire instead of being revoked.
|
||||
func WithContext(ctx context.Context) SessionOption {
|
||||
return func(so *sessionOptions) {
|
||||
so.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
@ -249,10 +249,11 @@ func (s *stmReadCommitted) commit() *v3.TxnResponse {
|
||||
}
|
||||
|
||||
func isKeyCurrent(k string, r *v3.GetResponse) v3.Cmp {
|
||||
rev := r.Header.Revision + 1
|
||||
if len(r.Kvs) != 0 {
|
||||
return v3.Compare(v3.ModRevision(k), "=", r.Kvs[0].ModRevision)
|
||||
rev = r.Kvs[0].ModRevision + 1
|
||||
}
|
||||
return v3.Compare(v3.ModRevision(k), "=", 0)
|
||||
return v3.Compare(v3.ModRevision(k), "<", rev)
|
||||
}
|
||||
|
||||
func respToValue(resp *v3.GetResponse) string {
|
||||
|
@ -28,16 +28,15 @@ type Config struct {
|
||||
// Endpoints is a list of URLs
|
||||
Endpoints []string
|
||||
|
||||
// AutoSyncInterval is the interval to update endpoints with its latest members.
|
||||
// 0 disables auto-sync. By default auto-sync is disabled.
|
||||
AutoSyncInterval time.Duration
|
||||
|
||||
// DialTimeout is the timeout for failing to establish a connection.
|
||||
DialTimeout time.Duration
|
||||
|
||||
// TLS holds the client secure credentials, if any.
|
||||
TLS *tls.Config
|
||||
|
||||
// Logger is the logger used by client library.
|
||||
Logger Logger
|
||||
|
||||
// Username is a username for authentication
|
||||
Username string
|
||||
|
||||
@ -47,7 +46,6 @@ type Config struct {
|
||||
|
||||
type yamlConfig struct {
|
||||
Endpoints []string `json:"endpoints"`
|
||||
AutoSyncInterval time.Duration `json:"auto-sync-interval"`
|
||||
DialTimeout time.Duration `json:"dial-timeout"`
|
||||
InsecureTransport bool `json:"insecure-transport"`
|
||||
InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify"`
|
||||
@ -70,9 +68,8 @@ func configFromFile(fpath string) (*Config, error) {
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
Endpoints: yc.Endpoints,
|
||||
AutoSyncInterval: yc.AutoSyncInterval,
|
||||
DialTimeout: yc.DialTimeout,
|
||||
Endpoints: yc.Endpoints,
|
||||
DialTimeout: yc.DialTimeout,
|
||||
}
|
||||
|
||||
if yc.InsecureTransport {
|
||||
|
@ -44,7 +44,7 @@
|
||||
// etcd client returns 2 types of errors:
|
||||
//
|
||||
// 1. context error: canceled or deadline exceeded.
|
||||
// 2. gRPC error: see https://github.com/coreos/etcd/blob/master/etcdserver/api/v3rpc/rpctypes/error.go
|
||||
// 2. gRPC error: see https://github.com/coreos/etcd/blob/master/etcdserver/api/v3rpc/error.go.
|
||||
//
|
||||
// Here is the example code to handle client errors:
|
||||
//
|
||||
|
@ -1,46 +0,0 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clientv3_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func ExampleMetrics_All() {
|
||||
// listen for all prometheus metrics
|
||||
go func() {
|
||||
http.Handle("/metrics", prometheus.Handler())
|
||||
log.Fatal(http.ListenAndServe(":47989", nil))
|
||||
}()
|
||||
|
||||
url := "http://localhost:47989/metrics"
|
||||
|
||||
// make an http request to fetch all prometheus metrics
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Fatalf("fetch error: %v", err)
|
||||
}
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
log.Fatalf("fetch error: reading %s: %v", url, err)
|
||||
}
|
||||
fmt.Printf("%s", b)
|
||||
}
|
@ -19,8 +19,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@ -31,9 +29,6 @@ var (
|
||||
)
|
||||
|
||||
func Example() {
|
||||
var plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "clientv3")
|
||||
clientv3.SetLogger(plog)
|
||||
|
||||
cli, err := clientv3.New(clientv3.Config{
|
||||
Endpoints: endpoints,
|
||||
DialTimeout: dialTimeout,
|
||||
@ -48,29 +43,3 @@ func Example() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleConfig_withTLS() {
|
||||
tlsInfo := transport.TLSInfo{
|
||||
CertFile: "/tmp/test-certs/test-name-1.pem",
|
||||
KeyFile: "/tmp/test-certs/test-name-1-key.pem",
|
||||
TrustedCAFile: "/tmp/test-certs/trusted-ca.pem",
|
||||
}
|
||||
tlsConfig, err := tlsInfo.ClientConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cli, err := clientv3.New(clientv3.Config{
|
||||
Endpoints: endpoints,
|
||||
DialTimeout: dialTimeout,
|
||||
TLS: tlsConfig,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cli.Close() // make sure to close the client
|
||||
|
||||
_, err = cli.Put(context.TODO(), "foo", "bar")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/integration"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// TestDialSetEndpoints ensures SetEndpoints can replace unavailable endpoints with available ones.
|
||||
func TestDialSetEndpoints(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 3})
|
||||
defer clus.Terminate(t)
|
||||
|
||||
// get endpoint list
|
||||
eps := make([]string, 3)
|
||||
for i := range eps {
|
||||
eps[i] = clus.Members[i].GRPCAddr()
|
||||
}
|
||||
toKill := rand.Intn(len(eps))
|
||||
|
||||
cfg := clientv3.Config{Endpoints: []string{eps[toKill]}, DialTimeout: 1 * time.Second}
|
||||
cli, err := clientv3.New(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
// make a dead node
|
||||
clus.Members[toKill].Stop(t)
|
||||
clus.WaitLeader(t)
|
||||
|
||||
// update client with available endpoints
|
||||
cli.SetEndpoints(eps[(toKill+1)%3])
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
if _, err = cli.Get(ctx, "foo", clientv3.WithSerializable()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cancel()
|
||||
}
|
@ -17,7 +17,6 @@ package integration
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -36,8 +35,8 @@ func TestKVPutError(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
var (
|
||||
maxReqBytes = 1.5 * 1024 * 1024 // hard coded max in v3_server.go
|
||||
quota = int64(int(maxReqBytes) + 8*os.Getpagesize())
|
||||
maxReqBytes = 1.5 * 1024 * 1024
|
||||
quota = int64(maxReqBytes * 1.2)
|
||||
)
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1, QuotaBackendBytes: quota})
|
||||
defer clus.Terminate(t)
|
||||
@ -50,7 +49,7 @@ func TestKVPutError(t *testing.T) {
|
||||
t.Fatalf("expected %v, got %v", rpctypes.ErrEmptyKey, err)
|
||||
}
|
||||
|
||||
_, err = kv.Put(ctx, "key", strings.Repeat("a", int(maxReqBytes+100)))
|
||||
_, err = kv.Put(ctx, "key", strings.Repeat("a", int(maxReqBytes+100))) // 1.5MB
|
||||
if err != rpctypes.ErrRequestTooLarge {
|
||||
t.Fatalf("expected %v, got %v", rpctypes.ErrRequestTooLarge, err)
|
||||
}
|
||||
@ -60,7 +59,7 @@ func TestKVPutError(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second) // give enough time for commit
|
||||
time.Sleep(500 * time.Millisecond) // give enough time for commit
|
||||
|
||||
_, err = kv.Put(ctx, "foo2", strings.Repeat("a", int(maxReqBytes-50)))
|
||||
if err != rpctypes.ErrNoSpace { // over quota
|
||||
@ -226,21 +225,6 @@ func TestKVRange(t *testing.T) {
|
||||
{Key: []byte("fop"), Value: nil, CreateRevision: 9, ModRevision: 9, Version: 1},
|
||||
},
|
||||
},
|
||||
// range all with SortByKey, missing sorting order (ASCEND by default)
|
||||
{
|
||||
"a", "x",
|
||||
0,
|
||||
[]clientv3.OpOption{clientv3.WithSort(clientv3.SortByKey, clientv3.SortNone)},
|
||||
|
||||
[]*mvccpb.KeyValue{
|
||||
{Key: []byte("a"), Value: nil, CreateRevision: 2, ModRevision: 2, Version: 1},
|
||||
{Key: []byte("b"), Value: nil, CreateRevision: 3, ModRevision: 3, Version: 1},
|
||||
{Key: []byte("c"), Value: nil, CreateRevision: 4, ModRevision: 6, Version: 3},
|
||||
{Key: []byte("foo"), Value: nil, CreateRevision: 7, ModRevision: 7, Version: 1},
|
||||
{Key: []byte("foo/abc"), Value: nil, CreateRevision: 8, ModRevision: 8, Version: 1},
|
||||
{Key: []byte("fop"), Value: nil, CreateRevision: 9, ModRevision: 9, Version: 1},
|
||||
},
|
||||
},
|
||||
// range all with SortByCreateRevision, SortDescend
|
||||
{
|
||||
"a", "x",
|
||||
@ -256,21 +240,6 @@ func TestKVRange(t *testing.T) {
|
||||
{Key: []byte("a"), Value: nil, CreateRevision: 2, ModRevision: 2, Version: 1},
|
||||
},
|
||||
},
|
||||
// range all with SortByCreateRevision, missing sorting order (ASCEND by default)
|
||||
{
|
||||
"a", "x",
|
||||
0,
|
||||
[]clientv3.OpOption{clientv3.WithSort(clientv3.SortByCreateRevision, clientv3.SortNone)},
|
||||
|
||||
[]*mvccpb.KeyValue{
|
||||
{Key: []byte("a"), Value: nil, CreateRevision: 2, ModRevision: 2, Version: 1},
|
||||
{Key: []byte("b"), Value: nil, CreateRevision: 3, ModRevision: 3, Version: 1},
|
||||
{Key: []byte("c"), Value: nil, CreateRevision: 4, ModRevision: 6, Version: 3},
|
||||
{Key: []byte("foo"), Value: nil, CreateRevision: 7, ModRevision: 7, Version: 1},
|
||||
{Key: []byte("foo/abc"), Value: nil, CreateRevision: 8, ModRevision: 8, Version: 1},
|
||||
{Key: []byte("fop"), Value: nil, CreateRevision: 9, ModRevision: 9, Version: 1},
|
||||
},
|
||||
},
|
||||
// range all with SortByModRevision, SortDescend
|
||||
{
|
||||
"a", "x",
|
||||
@ -679,47 +648,16 @@ func TestKVGetCancel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestKVGetStoppedServerAndClose ensures closing after a failed Get works.
|
||||
func TestKVGetStoppedServerAndClose(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer clus.Terminate(t)
|
||||
|
||||
cli := clus.Client(0)
|
||||
clus.Members[0].Stop(t)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
|
||||
// this Get fails and triggers an asynchronous connection retry
|
||||
_, err := cli.Get(ctx, "abc")
|
||||
cancel()
|
||||
if !strings.Contains(err.Error(), "context deadline") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKVPutStoppedServerAndClose ensures closing after a failed Put works.
|
||||
func TestKVPutStoppedServerAndClose(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer clus.Terminate(t)
|
||||
|
||||
cli := clus.Client(0)
|
||||
clus.Members[0].Stop(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
|
||||
// get retries on all errors.
|
||||
// so here we use it to eat the potential broken pipe error for the next put.
|
||||
// grpc client might see a broken pipe error when we issue the get request before
|
||||
// grpc finds out the original connection is down due to the member shutdown.
|
||||
_, err := cli.Get(ctx, "abc")
|
||||
cancel()
|
||||
if !strings.Contains(err.Error(), "context deadline") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// this Put fails and triggers an asynchronous connection retry
|
||||
_, err = cli.Put(ctx, "abc", "123")
|
||||
_, err := cli.Put(ctx, "abc", "123")
|
||||
cancel()
|
||||
if !strings.Contains(err.Error(), "context deadline") {
|
||||
t.Fatal(err)
|
||||
|
@ -15,14 +15,10 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/clientv3/concurrency"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
"github.com/coreos/etcd/integration"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
@ -459,174 +455,3 @@ func TestLeaseKeepAliveTTLTimeout(t *testing.T) {
|
||||
|
||||
clus.Members[0].Restart(t)
|
||||
}
|
||||
|
||||
func TestLeaseTimeToLive(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 3})
|
||||
defer clus.Terminate(t)
|
||||
|
||||
lapi := clientv3.NewLease(clus.RandClient())
|
||||
defer lapi.Close()
|
||||
|
||||
resp, err := lapi.Grant(context.Background(), 10)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create lease %v", err)
|
||||
}
|
||||
|
||||
kv := clientv3.NewKV(clus.RandClient())
|
||||
keys := []string{"foo1", "foo2"}
|
||||
for i := range keys {
|
||||
if _, err = kv.Put(context.TODO(), keys[i], "bar", clientv3.WithLease(resp.ID)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
lresp, lerr := lapi.TimeToLive(context.Background(), resp.ID, clientv3.WithAttachedKeys())
|
||||
if lerr != nil {
|
||||
t.Fatal(lerr)
|
||||
}
|
||||
if lresp.ID != resp.ID {
|
||||
t.Fatalf("leaseID expected %d, got %d", resp.ID, lresp.ID)
|
||||
}
|
||||
if lresp.GrantedTTL != int64(10) {
|
||||
t.Fatalf("GrantedTTL expected %d, got %d", 10, lresp.GrantedTTL)
|
||||
}
|
||||
if lresp.TTL == 0 || lresp.TTL > lresp.GrantedTTL {
|
||||
t.Fatalf("unexpected TTL %d (granted %d)", lresp.TTL, lresp.GrantedTTL)
|
||||
}
|
||||
ks := make([]string, len(lresp.Keys))
|
||||
for i := range lresp.Keys {
|
||||
ks[i] = string(lresp.Keys[i])
|
||||
}
|
||||
sort.Strings(ks)
|
||||
if !reflect.DeepEqual(ks, keys) {
|
||||
t.Fatalf("keys expected %v, got %v", keys, ks)
|
||||
}
|
||||
|
||||
lresp, lerr = lapi.TimeToLive(context.Background(), resp.ID)
|
||||
if lerr != nil {
|
||||
t.Fatal(lerr)
|
||||
}
|
||||
if len(lresp.Keys) != 0 {
|
||||
t.Fatalf("unexpected keys %+v", lresp.Keys)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLeaseRenewLostQuorum ensures keepalives work after losing quorum
|
||||
// for a while.
|
||||
func TestLeaseRenewLostQuorum(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 3})
|
||||
defer clus.Terminate(t)
|
||||
|
||||
cli := clus.Client(0)
|
||||
r, err := cli.Grant(context.TODO(), 4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
kctx, kcancel := context.WithCancel(context.Background())
|
||||
defer kcancel()
|
||||
ka, err := cli.KeepAlive(kctx, r.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// consume first keepalive so next message sends when cluster is down
|
||||
<-ka
|
||||
|
||||
// force keepalive stream message to timeout
|
||||
clus.Members[1].Stop(t)
|
||||
clus.Members[2].Stop(t)
|
||||
// Use TTL-1 since the client closes the keepalive channel if no
|
||||
// keepalive arrives before the lease deadline.
|
||||
// The cluster has 1 second to recover and reply to the keepalive.
|
||||
time.Sleep(time.Duration(r.TTL-1) * time.Second)
|
||||
clus.Members[1].Restart(t)
|
||||
clus.Members[2].Restart(t)
|
||||
|
||||
select {
|
||||
case _, ok := <-ka:
|
||||
if !ok {
|
||||
t.Fatalf("keepalive closed")
|
||||
}
|
||||
case <-time.After(time.Duration(r.TTL) * time.Second):
|
||||
t.Fatalf("timed out waiting for keepalive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeaseKeepAliveLoopExit(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer clus.Terminate(t)
|
||||
|
||||
ctx := context.Background()
|
||||
cli := clus.Client(0)
|
||||
|
||||
resp, err := cli.Grant(ctx, 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cli.Lease.Close()
|
||||
|
||||
_, err = cli.KeepAlive(ctx, resp.ID)
|
||||
if _, ok := err.(clientv3.ErrKeepAliveHalted); !ok {
|
||||
t.Fatalf("expected %T, got %v(%T)", clientv3.ErrKeepAliveHalted{}, err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestV3LeaseFailureOverlap issues Grant and Keepalive requests to a cluster
|
||||
// before, during, and after quorum loss to confirm Grant/Keepalive tolerates
|
||||
// transient cluster failure.
|
||||
func TestV3LeaseFailureOverlap(t *testing.T) {
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 2})
|
||||
defer clus.Terminate(t)
|
||||
|
||||
numReqs := 5
|
||||
cli := clus.Client(0)
|
||||
|
||||
// bring up a session, tear it down
|
||||
updown := func(i int) error {
|
||||
sess, err := concurrency.NewSession(cli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
defer close(ch)
|
||||
sess.Close()
|
||||
}()
|
||||
select {
|
||||
case <-ch:
|
||||
case <-time.After(time.Minute / 4):
|
||||
t.Fatalf("timeout %d", i)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
mkReqs := func(n int) {
|
||||
wg.Add(numReqs)
|
||||
for i := 0; i < numReqs; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := updown(n)
|
||||
if err == nil || err == rpctypes.ErrTimeoutDueToConnectionLost {
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
mkReqs(1)
|
||||
clus.Members[1].Stop(t)
|
||||
mkReqs(2)
|
||||
time.Sleep(time.Second)
|
||||
mkReqs(3)
|
||||
clus.Members[1].Restart(t)
|
||||
mkReqs(4)
|
||||
wg.Wait()
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import "github.com/coreos/pkg/capnslog"
|
||||
|
||||
func init() {
|
||||
capnslog.SetGlobalLogLevel(capnslog.INFO)
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/integration"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestV3ClientMetrics(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
var (
|
||||
addr string = "localhost:27989"
|
||||
ln net.Listener
|
||||
err error
|
||||
)
|
||||
|
||||
// listen for all prometheus metrics
|
||||
donec := make(chan struct{})
|
||||
go func() {
|
||||
defer close(donec)
|
||||
|
||||
srv := &http.Server{Handler: prometheus.Handler()}
|
||||
srv.SetKeepAlivesEnabled(false)
|
||||
|
||||
ln, err = transport.NewUnixListener(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v occurred while listening on addr: %v", err, addr)
|
||||
}
|
||||
|
||||
err = srv.Serve(ln)
|
||||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
t.Fatalf("Err serving http requests: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
url := "unix://" + addr + "/metrics"
|
||||
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer clus.Terminate(t)
|
||||
|
||||
client := clus.Client(0)
|
||||
|
||||
w := clientv3.NewWatcher(client)
|
||||
defer w.Close()
|
||||
|
||||
kv := clientv3.NewKV(client)
|
||||
|
||||
wc := w.Watch(context.Background(), "foo")
|
||||
|
||||
wBefore := sumCountersForMetricAndLabels(t, url, "grpc_client_msg_received_total", "Watch", "bidi_stream")
|
||||
|
||||
pBefore := sumCountersForMetricAndLabels(t, url, "grpc_client_started_total", "Put", "unary")
|
||||
|
||||
_, err = kv.Put(context.Background(), "foo", "bar")
|
||||
if err != nil {
|
||||
t.Errorf("Error putting value in key store")
|
||||
}
|
||||
|
||||
pAfter := sumCountersForMetricAndLabels(t, url, "grpc_client_started_total", "Put", "unary")
|
||||
if pBefore+1 != pAfter {
|
||||
t.Errorf("grpc_client_started_total expected %d, got %d", 1, pAfter-pBefore)
|
||||
}
|
||||
|
||||
// consume watch response
|
||||
select {
|
||||
case <-wc:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Error("Timeout occurred for getting watch response")
|
||||
}
|
||||
|
||||
wAfter := sumCountersForMetricAndLabels(t, url, "grpc_client_msg_received_total", "Watch", "bidi_stream")
|
||||
if wBefore+1 != wAfter {
|
||||
t.Errorf("grpc_client_msg_received_total expected %d, got %d", 1, wAfter-wBefore)
|
||||
}
|
||||
|
||||
ln.Close()
|
||||
<-donec
|
||||
}
|
||||
|
||||
func sumCountersForMetricAndLabels(t *testing.T, url string, metricName string, matchingLabelValues ...string) int {
|
||||
count := 0
|
||||
for _, line := range getHTTPBodyAsLines(t, url) {
|
||||
ok := true
|
||||
if !strings.HasPrefix(line, metricName) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, labelValue := range matchingLabelValues {
|
||||
if !strings.Contains(line, `"`+labelValue+`"`) {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
valueString := line[strings.LastIndex(line, " ")+1 : len(line)-1]
|
||||
valueFloat, err := strconv.ParseFloat(valueString, 32)
|
||||
if err != nil {
|
||||
t.Fatalf("failed parsing value for line: %v and matchingLabelValues: %v", line, matchingLabelValues)
|
||||
}
|
||||
count += int(valueFloat)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func getHTTPBodyAsLines(t *testing.T, url string) []string {
|
||||
cfgtls := transport.TLSInfo{}
|
||||
tr, err := transport.NewTransport(cfgtls, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting transport: %v", err)
|
||||
}
|
||||
|
||||
tr.MaxIdleConns = -1
|
||||
tr.DisableKeepAlives = true
|
||||
|
||||
cli := &http.Client{Transport: tr}
|
||||
|
||||
resp, err := cli.Get(url)
|
||||
if err != nil {
|
||||
t.Fatalf("Error fetching: %v", err)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
lines := []string{}
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else {
|
||||
t.Fatalf("error reading: %v", err)
|
||||
}
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
resp.Body.Close()
|
||||
return lines
|
||||
}
|
@ -73,7 +73,6 @@ func TestTxnWriteFail(t *testing.T) {
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer close(getc)
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("timed out waiting for txn fail")
|
||||
@ -87,10 +86,11 @@ func TestTxnWriteFail(t *testing.T) {
|
||||
if len(gresp.Kvs) != 0 {
|
||||
t.Fatalf("expected no keys, got %v", gresp.Kvs)
|
||||
}
|
||||
close(getc)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(2 * clus.Members[1].ServerConfig.ReqTimeout()):
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("timed out waiting for get")
|
||||
case <-getc:
|
||||
}
|
||||
@ -125,7 +125,7 @@ func TestTxnReadRetry(t *testing.T) {
|
||||
clus.Members[0].Restart(t)
|
||||
select {
|
||||
case <-donec:
|
||||
case <-time.After(2 * clus.Members[1].ServerConfig.ReqTimeout()):
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("waited too long")
|
||||
}
|
||||
}
|
||||
|
@ -211,14 +211,6 @@ func testWatchReconnRequest(t *testing.T, wctx *watchctx) {
|
||||
stopc <- struct{}{}
|
||||
<-donec
|
||||
|
||||
// spinning on dropping connections may trigger a leader election
|
||||
// due to resource starvation; l-read to ensure the cluster is stable
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
|
||||
if _, err := wctx.kv.Get(ctx, "_"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cancel()
|
||||
|
||||
// ensure watcher works
|
||||
putAndWatch(t, wctx, "a", "a")
|
||||
}
|
||||
@ -349,9 +341,6 @@ func putAndWatch(t *testing.T, wctx *watchctx, key, val string) {
|
||||
|
||||
// TestWatchResumeComapcted checks that the watcher gracefully closes in case
|
||||
// that it tries to resume to a revision that's been compacted out of the store.
|
||||
// Since the watcher's server restarts with stale data, the watcher will receive
|
||||
// either a compaction error or all keys by staying in sync before the compaction
|
||||
// is finally applied.
|
||||
func TestWatchResumeCompacted(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
@ -380,9 +369,8 @@ func TestWatchResumeCompacted(t *testing.T) {
|
||||
}
|
||||
|
||||
// put some data and compact away
|
||||
numPuts := 5
|
||||
kv := clientv3.NewKV(clus.Client(1))
|
||||
for i := 0; i < numPuts; i++ {
|
||||
for i := 0; i < 5; i++ {
|
||||
if _, err := kv.Put(context.TODO(), "foo", "bar"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -393,48 +381,17 @@ func TestWatchResumeCompacted(t *testing.T) {
|
||||
|
||||
clus.Members[0].Restart(t)
|
||||
|
||||
// since watch's server isn't guaranteed to be synced with the cluster when
|
||||
// the watch resumes, there is a window where the watch can stay synced and
|
||||
// read off all events; if the watcher misses the window, it will go out of
|
||||
// sync and get a compaction error.
|
||||
wRev := int64(2)
|
||||
for int(wRev) <= numPuts+1 {
|
||||
var wresp clientv3.WatchResponse
|
||||
var ok bool
|
||||
select {
|
||||
case wresp, ok = <-wch:
|
||||
if !ok {
|
||||
t.Fatalf("expected wresp, but got closed channel")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("compacted watch timed out")
|
||||
}
|
||||
for _, ev := range wresp.Events {
|
||||
if ev.Kv.ModRevision != wRev {
|
||||
t.Fatalf("expected modRev %v, got %+v", wRev, ev)
|
||||
}
|
||||
wRev++
|
||||
}
|
||||
if wresp.Err() == nil {
|
||||
continue
|
||||
}
|
||||
if wresp.Err() != rpctypes.ErrCompacted {
|
||||
t.Fatalf("wresp.Err() expected %v, but got %v %+v", rpctypes.ErrCompacted, wresp.Err())
|
||||
}
|
||||
break
|
||||
// get compacted error message
|
||||
wresp, ok := <-wch
|
||||
if !ok {
|
||||
t.Fatalf("expected wresp, but got closed channel")
|
||||
}
|
||||
if int(wRev) > numPuts+1 {
|
||||
// got data faster than the compaction
|
||||
return
|
||||
if wresp.Err() != rpctypes.ErrCompacted {
|
||||
t.Fatalf("wresp.Err() expected %v, but got %v", rpctypes.ErrCompacted, wresp.Err())
|
||||
}
|
||||
// received compaction error; ensure the channel closes
|
||||
select {
|
||||
case wresp, ok := <-wch:
|
||||
if ok {
|
||||
t.Fatalf("expected closed channel, but got %v", wresp)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("timed out waiting for channel close")
|
||||
// ensure the channel is closed
|
||||
if wresp, ok = <-wch; ok {
|
||||
t.Fatalf("expected closed channel, but got %v", wresp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -614,19 +571,18 @@ func TestWatchErrConnClosed(t *testing.T) {
|
||||
defer clus.Terminate(t)
|
||||
|
||||
cli := clus.Client(0)
|
||||
defer cli.Close()
|
||||
wc := clientv3.NewWatcher(cli)
|
||||
|
||||
donec := make(chan struct{})
|
||||
go func() {
|
||||
defer close(donec)
|
||||
ch := wc.Watch(context.TODO(), "foo")
|
||||
if wr := <-ch; grpc.ErrorDesc(wr.Err()) != grpc.ErrClientConnClosing.Error() {
|
||||
t.Fatalf("expected %v, got %v", grpc.ErrClientConnClosing, grpc.ErrorDesc(wr.Err()))
|
||||
wc.Watch(context.TODO(), "foo")
|
||||
if err := wc.Close(); err != nil && err != grpc.ErrClientConnClosing {
|
||||
t.Fatalf("expected %v, got %v", grpc.ErrClientConnClosing, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cli.ActiveConnection().Close(); err != nil {
|
||||
if err := cli.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clus.TakeClient(0)
|
||||
@ -673,12 +629,8 @@ func TestWatchWithRequireLeader(t *testing.T) {
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 3})
|
||||
defer clus.Terminate(t)
|
||||
|
||||
// Put a key for the non-require leader watch to read as an event.
|
||||
// The watchers will be on member[0]; put key through member[0] to
|
||||
// ensure that it receives the update so watching after killing quorum
|
||||
// is guaranteed to have the key.
|
||||
liveClient := clus.Client(0)
|
||||
if _, err := liveClient.Put(context.TODO(), "foo", "bar"); err != nil {
|
||||
// something for the non-require leader watch to read as an event
|
||||
if _, err := clus.Client(1).Put(context.TODO(), "foo", "bar"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -693,8 +645,8 @@ func TestWatchWithRequireLeader(t *testing.T) {
|
||||
tickDuration := 10 * time.Millisecond
|
||||
time.Sleep(time.Duration(3*clus.Members[0].ElectionTicks) * tickDuration)
|
||||
|
||||
chLeader := liveClient.Watch(clientv3.WithRequireLeader(context.TODO()), "foo", clientv3.WithRev(1))
|
||||
chNoLeader := liveClient.Watch(context.TODO(), "foo", clientv3.WithRev(1))
|
||||
chLeader := clus.Client(0).Watch(clientv3.WithRequireLeader(context.TODO()), "foo", clientv3.WithRev(1))
|
||||
chNoLeader := clus.Client(0).Watch(context.TODO(), "foo", clientv3.WithRev(1))
|
||||
|
||||
select {
|
||||
case resp, ok := <-chLeader:
|
||||
@ -722,114 +674,6 @@ func TestWatchWithRequireLeader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatchWithFilter checks that watch filtering works.
|
||||
func TestWatchWithFilter(t *testing.T) {
|
||||
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer cluster.Terminate(t)
|
||||
|
||||
client := cluster.RandClient()
|
||||
ctx := context.Background()
|
||||
|
||||
wcNoPut := client.Watch(ctx, "a", clientv3.WithFilterPut())
|
||||
wcNoDel := client.Watch(ctx, "a", clientv3.WithFilterDelete())
|
||||
|
||||
if _, err := client.Put(ctx, "a", "abc"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := client.Delete(ctx, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
npResp := <-wcNoPut
|
||||
if len(npResp.Events) != 1 || npResp.Events[0].Type != clientv3.EventTypeDelete {
|
||||
t.Fatalf("expected delete event, got %+v", npResp.Events)
|
||||
}
|
||||
ndResp := <-wcNoDel
|
||||
if len(ndResp.Events) != 1 || ndResp.Events[0].Type != clientv3.EventTypePut {
|
||||
t.Fatalf("expected put event, got %+v", ndResp.Events)
|
||||
}
|
||||
|
||||
select {
|
||||
case resp := <-wcNoPut:
|
||||
t.Fatalf("unexpected event on filtered put (%+v)", resp)
|
||||
case resp := <-wcNoDel:
|
||||
t.Fatalf("unexpected event on filtered delete (%+v)", resp)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatchWithCreatedNotification checks that createdNotification works.
|
||||
func TestWatchWithCreatedNotification(t *testing.T) {
|
||||
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer cluster.Terminate(t)
|
||||
|
||||
client := cluster.RandClient()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
createC := client.Watch(ctx, "a", clientv3.WithCreatedNotify())
|
||||
|
||||
resp := <-createC
|
||||
|
||||
if !resp.Created {
|
||||
t.Fatalf("expected created event, got %v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatchWithCreatedNotificationDropConn ensures that
|
||||
// a watcher with created notify does not post duplicate
|
||||
// created events from disconnect.
|
||||
func TestWatchWithCreatedNotificationDropConn(t *testing.T) {
|
||||
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer cluster.Terminate(t)
|
||||
|
||||
client := cluster.RandClient()
|
||||
|
||||
wch := client.Watch(context.Background(), "a", clientv3.WithCreatedNotify())
|
||||
|
||||
resp := <-wch
|
||||
|
||||
if !resp.Created {
|
||||
t.Fatalf("expected created event, got %v", resp)
|
||||
}
|
||||
|
||||
cluster.Members[0].DropConnections()
|
||||
|
||||
// try to receive from watch channel again
|
||||
// ensure it doesn't post another createNotify
|
||||
select {
|
||||
case wresp := <-wch:
|
||||
t.Fatalf("got unexpected watch response: %+v\n", wresp)
|
||||
case <-time.After(time.Second):
|
||||
// watcher may not reconnect by the time it hits the select,
|
||||
// so it wouldn't have a chance to filter out the second create event
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatchCancelOnServer ensures client watcher cancels propagate back to the server.
|
||||
func TestWatchCancelOnServer(t *testing.T) {
|
||||
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer cluster.Terminate(t)
|
||||
|
||||
client := cluster.RandClient()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
client.Watch(ctx, "a", clientv3.WithCreatedNotify())
|
||||
cancel()
|
||||
}
|
||||
// wait for cancels to propagate
|
||||
time.Sleep(time.Second)
|
||||
|
||||
watchers, err := cluster.Members[0].Metric("etcd_debugging_mvcc_watcher_total")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if watchers != "0" {
|
||||
t.Fatalf("expected 0 watchers, got %q", watchers)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatchOverlapContextCancel stresses the watcher stream teardown path by
|
||||
// creating/canceling watchers to ensure that new watchers are not taken down
|
||||
// by a torn down watch stream. The sort of race that's being detected:
|
||||
@ -938,45 +782,3 @@ func TestWatchCancelAndCloseClient(t *testing.T) {
|
||||
<-donec
|
||||
clus.TakeClient(0)
|
||||
}
|
||||
|
||||
// TestWatchStressResumeClose establishes a bunch of watchers, disconnects
|
||||
// to put them in resuming mode, cancels them so some resumes by cancel fail,
|
||||
// then closes the watcher interface to ensure correct clean up.
|
||||
func TestWatchStressResumeClose(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer clus.Terminate(t)
|
||||
cli := clus.Client(0)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
// add more watches than can be resumed before the cancel
|
||||
wchs := make([]clientv3.WatchChan, 2000)
|
||||
for i := range wchs {
|
||||
wchs[i] = cli.Watch(ctx, "abc")
|
||||
}
|
||||
clus.Members[0].DropConnections()
|
||||
cancel()
|
||||
if err := cli.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clus.TakeClient(0)
|
||||
}
|
||||
|
||||
// TestWatchCancelDisconnected ensures canceling a watcher works when
|
||||
// its grpc stream is disconnected / reconnecting.
|
||||
func TestWatchCancelDisconnected(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer clus.Terminate(t)
|
||||
cli := clus.Client(0)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
// add more watches than can be resumed before the cancel
|
||||
wch := cli.Watch(ctx, "abc")
|
||||
clus.Members[0].Stop(t)
|
||||
cancel()
|
||||
select {
|
||||
case <-wch:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("took too long to cancel disconnected watcher")
|
||||
}
|
||||
}
|
||||
|
@ -85,10 +85,6 @@ func NewKV(c *Client) KV {
|
||||
return &kv{remote: RetryKVClient(c)}
|
||||
}
|
||||
|
||||
func NewKVFromKVClient(remote pb.KVClient) KV {
|
||||
return &kv{remote: remote}
|
||||
}
|
||||
|
||||
func (kv *kv) Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error) {
|
||||
r, err := kv.Do(ctx, OpPut(key, val, opts...))
|
||||
return r.put, toErr(ctx, err)
|
||||
@ -105,7 +101,7 @@ func (kv *kv) Delete(ctx context.Context, key string, opts ...OpOption) (*Delete
|
||||
}
|
||||
|
||||
func (kv *kv) Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error) {
|
||||
resp, err := kv.remote.Compact(ctx, OpCompact(rev, opts...).toRequest())
|
||||
resp, err := kv.remote.Compact(ctx, OpCompact(rev, opts...).toRequest(), grpc.FailFast(false))
|
||||
if err != nil {
|
||||
return nil, toErr(ctx, err)
|
||||
}
|
||||
@ -125,7 +121,6 @@ func (kv *kv) Do(ctx context.Context, op Op) (OpResponse, error) {
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if isHaltErr(ctx, err) {
|
||||
return resp, toErr(ctx, err)
|
||||
}
|
||||
@ -142,7 +137,21 @@ func (kv *kv) do(ctx context.Context, op Op) (OpResponse, error) {
|
||||
// TODO: handle other ops
|
||||
case tRange:
|
||||
var resp *pb.RangeResponse
|
||||
resp, err = kv.remote.Range(ctx, op.toRangeRequest(), grpc.FailFast(false))
|
||||
r := &pb.RangeRequest{
|
||||
Key: op.key,
|
||||
RangeEnd: op.end,
|
||||
Limit: op.limit,
|
||||
Revision: op.rev,
|
||||
Serializable: op.serializable,
|
||||
KeysOnly: op.keysOnly,
|
||||
CountOnly: op.countOnly,
|
||||
}
|
||||
if op.sort != nil {
|
||||
r.SortOrder = pb.RangeRequest_SortOrder(op.sort.Order)
|
||||
r.SortTarget = pb.RangeRequest_SortTarget(op.sort.Target)
|
||||
}
|
||||
|
||||
resp, err = kv.remote.Range(ctx, r, grpc.FailFast(false))
|
||||
if err == nil {
|
||||
return OpResponse{get: (*GetResponse)(resp)}, nil
|
||||
}
|
||||
|
@ -44,21 +44,6 @@ type LeaseKeepAliveResponse struct {
|
||||
TTL int64
|
||||
}
|
||||
|
||||
// LeaseTimeToLiveResponse is used to convert the protobuf lease timetolive response.
|
||||
type LeaseTimeToLiveResponse struct {
|
||||
*pb.ResponseHeader
|
||||
ID LeaseID `json:"id"`
|
||||
|
||||
// TTL is the remaining TTL in seconds for the lease; the lease will expire in under TTL+1 seconds.
|
||||
TTL int64 `json:"ttl"`
|
||||
|
||||
// GrantedTTL is the initial granted time in seconds upon lease creation/renewal.
|
||||
GrantedTTL int64 `json:"granted-ttl"`
|
||||
|
||||
// Keys is the list of keys attached to this lease.
|
||||
Keys [][]byte `json:"keys"`
|
||||
}
|
||||
|
||||
const (
|
||||
// defaultTTL is the assumed lease TTL used for the first keepalive
|
||||
// deadline before the actual TTL is known to the client.
|
||||
@ -69,21 +54,6 @@ const (
|
||||
NoLease LeaseID = 0
|
||||
)
|
||||
|
||||
// ErrKeepAliveHalted is returned if client keep alive loop halts with an unexpected error.
|
||||
//
|
||||
// This usually means that automatic lease renewal via KeepAlive is broken, but KeepAliveOnce will still work as expected.
|
||||
type ErrKeepAliveHalted struct {
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (e ErrKeepAliveHalted) Error() string {
|
||||
s := "etcdclient: leases keep alive halted"
|
||||
if e.Reason != nil {
|
||||
s += ": " + e.Reason.Error()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type Lease interface {
|
||||
// Grant creates a new lease.
|
||||
Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error)
|
||||
@ -91,9 +61,6 @@ type Lease interface {
|
||||
// Revoke revokes the given lease.
|
||||
Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error)
|
||||
|
||||
// TimeToLive retrieves the lease information of the given lease ID.
|
||||
TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error)
|
||||
|
||||
// KeepAlive keeps the given lease alive forever.
|
||||
KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)
|
||||
|
||||
@ -109,9 +76,8 @@ type Lease interface {
|
||||
type lessor struct {
|
||||
mu sync.Mutex // guards all fields
|
||||
|
||||
// donec is closed and loopErr is set when recvKeepAliveLoop stops
|
||||
donec chan struct{}
|
||||
loopErr error
|
||||
// donec is closed when recvKeepAliveLoop stops
|
||||
donec chan struct{}
|
||||
|
||||
remote pb.LeaseClient
|
||||
|
||||
@ -175,7 +141,10 @@ func (l *lessor) Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, err
|
||||
return gresp, nil
|
||||
}
|
||||
if isHaltErr(cctx, err) {
|
||||
return nil, toErr(cctx, err)
|
||||
return nil, toErr(ctx, err)
|
||||
}
|
||||
if nerr := l.newStream(); nerr != nil {
|
||||
return nil, nerr
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -195,29 +164,8 @@ func (l *lessor) Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse,
|
||||
if isHaltErr(ctx, err) {
|
||||
return nil, toErr(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lessor) TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error) {
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
done := cancelWhenStop(cancel, l.stopCtx.Done())
|
||||
defer close(done)
|
||||
|
||||
for {
|
||||
r := toLeaseTimeToLiveRequest(id, opts...)
|
||||
resp, err := l.remote.LeaseTimeToLive(cctx, r, grpc.FailFast(false))
|
||||
if err == nil {
|
||||
gresp := &LeaseTimeToLiveResponse{
|
||||
ResponseHeader: resp.GetHeader(),
|
||||
ID: LeaseID(resp.ID),
|
||||
TTL: resp.TTL,
|
||||
GrantedTTL: resp.GrantedTTL,
|
||||
Keys: resp.Keys,
|
||||
}
|
||||
return gresp, nil
|
||||
}
|
||||
if isHaltErr(cctx, err) {
|
||||
return nil, toErr(cctx, err)
|
||||
if nerr := l.newStream(); nerr != nil {
|
||||
return nil, nerr
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -226,15 +174,6 @@ func (l *lessor) KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAl
|
||||
ch := make(chan *LeaseKeepAliveResponse, leaseResponseChSize)
|
||||
|
||||
l.mu.Lock()
|
||||
// ensure that recvKeepAliveLoop is still running
|
||||
select {
|
||||
case <-l.donec:
|
||||
err := l.loopErr
|
||||
l.mu.Unlock()
|
||||
close(ch)
|
||||
return ch, ErrKeepAliveHalted{Reason: err}
|
||||
default:
|
||||
}
|
||||
ka, ok := l.keepAlives[id]
|
||||
if !ok {
|
||||
// create fresh keep alive
|
||||
@ -274,6 +213,10 @@ func (l *lessor) KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAlive
|
||||
if isHaltErr(ctx, err) {
|
||||
return nil, toErr(ctx, err)
|
||||
}
|
||||
|
||||
if nerr := l.newStream(); nerr != nil {
|
||||
return nil, nerr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,11 +285,10 @@ func (l *lessor) keepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAlive
|
||||
return karesp, nil
|
||||
}
|
||||
|
||||
func (l *lessor) recvKeepAliveLoop() (gerr error) {
|
||||
func (l *lessor) recvKeepAliveLoop() {
|
||||
defer func() {
|
||||
l.mu.Lock()
|
||||
close(l.donec)
|
||||
l.loopErr = gerr
|
||||
for _, ka := range l.keepAlives {
|
||||
ka.Close()
|
||||
}
|
||||
@ -359,35 +301,21 @@ func (l *lessor) recvKeepAliveLoop() (gerr error) {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
if isHaltErr(l.stopCtx, err) {
|
||||
return err
|
||||
return
|
||||
}
|
||||
stream, serr = l.resetRecv()
|
||||
continue
|
||||
}
|
||||
l.recvKeepAlive(resp)
|
||||
}
|
||||
return serr
|
||||
}
|
||||
|
||||
// resetRecv opens a new lease stream and starts sending LeaseKeepAliveRequests
|
||||
func (l *lessor) resetRecv() (pb.Lease_LeaseKeepAliveClient, error) {
|
||||
sctx, cancel := context.WithCancel(l.stopCtx)
|
||||
stream, err := l.remote.LeaseKeepAlive(sctx, grpc.FailFast(false))
|
||||
if err = toErr(sctx, err); err != nil {
|
||||
cancel()
|
||||
if err := l.newStream(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.stream != nil && l.streamCancel != nil {
|
||||
l.stream.CloseSend()
|
||||
l.streamCancel()
|
||||
}
|
||||
|
||||
l.streamCancel = cancel
|
||||
l.stream = stream
|
||||
|
||||
stream := l.getKeepAliveStream()
|
||||
go l.sendKeepAliveLoop(stream)
|
||||
return stream, nil
|
||||
}
|
||||
@ -416,7 +344,7 @@ func (l *lessor) recvKeepAlive(resp *pb.LeaseKeepAliveResponse) {
|
||||
}
|
||||
|
||||
// send update to all channels
|
||||
nextKeepAlive := time.Now().Add(time.Duration(karesp.TTL+2) / 3 * time.Second)
|
||||
nextKeepAlive := time.Now().Add(1 + time.Duration(karesp.TTL/3)*time.Second)
|
||||
ka.deadline = time.Now().Add(time.Duration(karesp.TTL) * time.Second)
|
||||
for _, ch := range ka.chs {
|
||||
select {
|
||||
@ -462,7 +390,7 @@ func (l *lessor) sendKeepAliveLoop(stream pb.Lease_LeaseKeepAliveClient) {
|
||||
return
|
||||
}
|
||||
|
||||
var tosend []LeaseID
|
||||
tosend := make([]LeaseID, 0)
|
||||
|
||||
now := time.Now()
|
||||
l.mu.Lock()
|
||||
@ -483,6 +411,32 @@ func (l *lessor) sendKeepAliveLoop(stream pb.Lease_LeaseKeepAliveClient) {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lessor) getKeepAliveStream() pb.Lease_LeaseKeepAliveClient {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.stream
|
||||
}
|
||||
|
||||
func (l *lessor) newStream() error {
|
||||
sctx, cancel := context.WithCancel(l.stopCtx)
|
||||
stream, err := l.remote.LeaseKeepAlive(sctx, grpc.FailFast(false))
|
||||
if err != nil {
|
||||
cancel()
|
||||
return toErr(sctx, err)
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.stream != nil && l.streamCancel != nil {
|
||||
l.stream.CloseSend()
|
||||
l.streamCancel()
|
||||
}
|
||||
|
||||
l.streamCancel = cancel
|
||||
l.stream = stream
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ka *keepAlive) Close() {
|
||||
close(ka.donec)
|
||||
for _, ch := range ka.chs {
|
||||
|
@ -15,15 +15,13 @@
|
||||
package clientv3
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
)
|
||||
|
||||
// Logger is the logger used by client library.
|
||||
// It implements grpclog.Logger interface.
|
||||
type Logger grpclog.Logger
|
||||
|
||||
var (
|
||||
@ -36,36 +34,20 @@ type settableLogger struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
// disable client side logs by default
|
||||
// use go's standard logger by default like grpc
|
||||
logger.mu.Lock()
|
||||
logger.l = log.New(ioutil.Discard, "", 0)
|
||||
|
||||
// logger has to override the grpclog at initialization so that
|
||||
// any changes to the grpclog go through logger with locking
|
||||
// instead of through SetLogger
|
||||
//
|
||||
// now updates only happen through settableLogger.set
|
||||
logger.l = log.New(os.Stderr, "", log.LstdFlags)
|
||||
grpclog.SetLogger(&logger)
|
||||
logger.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetLogger sets client-side Logger. By default, logs are disabled.
|
||||
func SetLogger(l Logger) {
|
||||
logger.set(l)
|
||||
}
|
||||
|
||||
// GetLogger returns the current logger.
|
||||
func GetLogger() Logger {
|
||||
return logger.get()
|
||||
}
|
||||
|
||||
func (s *settableLogger) set(l Logger) {
|
||||
func (s *settableLogger) Set(l Logger) {
|
||||
s.mu.Lock()
|
||||
logger.l = l
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *settableLogger) get() Logger {
|
||||
func (s *settableLogger) Get() Logger {
|
||||
s.mu.RLock()
|
||||
l := logger.l
|
||||
s.mu.RUnlock()
|
||||
@ -74,9 +56,9 @@ func (s *settableLogger) get() Logger {
|
||||
|
||||
// implement the grpclog.Logger interface
|
||||
|
||||
func (s *settableLogger) Fatal(args ...interface{}) { s.get().Fatal(args...) }
|
||||
func (s *settableLogger) Fatalf(format string, args ...interface{}) { s.get().Fatalf(format, args...) }
|
||||
func (s *settableLogger) Fatalln(args ...interface{}) { s.get().Fatalln(args...) }
|
||||
func (s *settableLogger) Print(args ...interface{}) { s.get().Print(args...) }
|
||||
func (s *settableLogger) Printf(format string, args ...interface{}) { s.get().Printf(format, args...) }
|
||||
func (s *settableLogger) Println(args ...interface{}) { s.get().Println(args...) }
|
||||
func (s *settableLogger) Fatal(args ...interface{}) { s.Get().Fatal(args...) }
|
||||
func (s *settableLogger) Fatalf(format string, args ...interface{}) { s.Get().Fatalf(format, args...) }
|
||||
func (s *settableLogger) Fatalln(args ...interface{}) { s.Get().Fatalln(args...) }
|
||||
func (s *settableLogger) Print(args ...interface{}) { s.Get().Print(args...) }
|
||||
func (s *settableLogger) Printf(format string, args ...interface{}) { s.Get().Printf(format, args...) }
|
||||
func (s *settableLogger) Println(args ...interface{}) { s.Get().Println(args...) }
|
||||
|
@ -20,14 +20,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/auth"
|
||||
"github.com/coreos/etcd/integration"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func init() { auth.BcryptCost = bcrypt.MinCost }
|
||||
|
||||
// TestMain sets up an etcd cluster if running the examples.
|
||||
func TestMain(m *testing.M) {
|
||||
useCluster := true // default to running all tests
|
||||
|
@ -1,128 +0,0 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package naming
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/naming"
|
||||
)
|
||||
|
||||
// GRPCResolver creates a grpc.Watcher for a target to track its resolution changes.
|
||||
type GRPCResolver struct {
|
||||
// Client is an initialized etcd client.
|
||||
Client *etcd.Client
|
||||
}
|
||||
|
||||
func (gr *GRPCResolver) Update(ctx context.Context, target string, nm naming.Update, opts ...etcd.OpOption) (err error) {
|
||||
switch nm.Op {
|
||||
case naming.Add:
|
||||
var v []byte
|
||||
if v, err = json.Marshal(nm); err != nil {
|
||||
return grpc.Errorf(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
_, err = gr.Client.KV.Put(ctx, target+"/"+nm.Addr, string(v), opts...)
|
||||
case naming.Delete:
|
||||
_, err = gr.Client.Delete(ctx, target+"/"+nm.Addr, opts...)
|
||||
default:
|
||||
return grpc.Errorf(codes.InvalidArgument, "naming: bad naming op")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (gr *GRPCResolver) Resolve(target string) (naming.Watcher, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
w := &gRPCWatcher{c: gr.Client, target: target + "/", ctx: ctx, cancel: cancel}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
type gRPCWatcher struct {
|
||||
c *etcd.Client
|
||||
target string
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wch etcd.WatchChan
|
||||
err error
|
||||
}
|
||||
|
||||
// Next gets the next set of updates from the etcd resolver.
|
||||
// Calls to Next should be serialized; concurrent calls are not safe since
|
||||
// there is no way to reconcile the update ordering.
|
||||
func (gw *gRPCWatcher) Next() ([]*naming.Update, error) {
|
||||
if gw.wch == nil {
|
||||
// first Next() returns all addresses
|
||||
return gw.firstNext()
|
||||
}
|
||||
if gw.err != nil {
|
||||
return nil, gw.err
|
||||
}
|
||||
|
||||
// process new events on target/*
|
||||
wr, ok := <-gw.wch
|
||||
if !ok {
|
||||
gw.err = grpc.Errorf(codes.Unavailable, "naming: watch closed")
|
||||
return nil, gw.err
|
||||
}
|
||||
if gw.err = wr.Err(); gw.err != nil {
|
||||
return nil, gw.err
|
||||
}
|
||||
|
||||
updates := make([]*naming.Update, 0, len(wr.Events))
|
||||
for _, e := range wr.Events {
|
||||
var jupdate naming.Update
|
||||
var err error
|
||||
switch e.Type {
|
||||
case etcd.EventTypePut:
|
||||
err = json.Unmarshal(e.Kv.Value, &jupdate)
|
||||
jupdate.Op = naming.Add
|
||||
case etcd.EventTypeDelete:
|
||||
err = json.Unmarshal(e.PrevKv.Value, &jupdate)
|
||||
jupdate.Op = naming.Delete
|
||||
}
|
||||
if err == nil {
|
||||
updates = append(updates, &jupdate)
|
||||
}
|
||||
}
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
func (gw *gRPCWatcher) firstNext() ([]*naming.Update, error) {
|
||||
// Use serialized request so resolution still works if the target etcd
|
||||
// server is partitioned away from the quorum.
|
||||
resp, err := gw.c.Get(gw.ctx, gw.target, etcd.WithPrefix(), etcd.WithSerializable())
|
||||
if gw.err = err; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updates := make([]*naming.Update, 0, len(resp.Kvs))
|
||||
for _, kv := range resp.Kvs {
|
||||
var jupdate naming.Update
|
||||
if err := json.Unmarshal(kv.Value, &jupdate); err != nil {
|
||||
continue
|
||||
}
|
||||
updates = append(updates, &jupdate)
|
||||
}
|
||||
|
||||
opts := []etcd.OpOption{etcd.WithRev(resp.Header.Revision + 1), etcd.WithPrefix(), etcd.WithPrevKV()}
|
||||
gw.wch = gw.c.Watch(gw.ctx, gw.target, opts...)
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
func (gw *gRPCWatcher) Close() { gw.cancel() }
|
@ -1,135 +0,0 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package naming
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/naming"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/integration"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
)
|
||||
|
||||
func TestGRPCResolver(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer clus.Terminate(t)
|
||||
|
||||
r := GRPCResolver{
|
||||
Client: clus.RandClient(),
|
||||
}
|
||||
|
||||
w, err := r.Resolve("foo")
|
||||
if err != nil {
|
||||
t.Fatal("failed to resolve foo", err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
addOp := naming.Update{Op: naming.Add, Addr: "127.0.0.1", Metadata: "metadata"}
|
||||
err = r.Update(context.TODO(), "foo", addOp)
|
||||
if err != nil {
|
||||
t.Fatal("failed to add foo", err)
|
||||
}
|
||||
|
||||
us, err := w.Next()
|
||||
if err != nil {
|
||||
t.Fatal("failed to get udpate", err)
|
||||
}
|
||||
|
||||
wu := &naming.Update{
|
||||
Op: naming.Add,
|
||||
Addr: "127.0.0.1",
|
||||
Metadata: "metadata",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(us[0], wu) {
|
||||
t.Fatalf("up = %#v, want %#v", us[0], wu)
|
||||
}
|
||||
|
||||
delOp := naming.Update{Op: naming.Delete, Addr: "127.0.0.1"}
|
||||
err = r.Update(context.TODO(), "foo", delOp)
|
||||
|
||||
us, err = w.Next()
|
||||
if err != nil {
|
||||
t.Fatal("failed to get udpate", err)
|
||||
}
|
||||
|
||||
wu = &naming.Update{
|
||||
Op: naming.Delete,
|
||||
Addr: "127.0.0.1",
|
||||
Metadata: "metadata",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(us[0], wu) {
|
||||
t.Fatalf("up = %#v, want %#v", us[0], wu)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGRPCResolverMultiInit ensures the resolver will initialize
|
||||
// correctly with multiple hosts and correctly receive multiple
|
||||
// updates in a single revision.
|
||||
func TestGRPCResolverMulti(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||
defer clus.Terminate(t)
|
||||
c := clus.RandClient()
|
||||
|
||||
v, verr := json.Marshal(naming.Update{Addr: "127.0.0.1", Metadata: "md"})
|
||||
if verr != nil {
|
||||
t.Fatal(verr)
|
||||
}
|
||||
if _, err := c.Put(context.TODO(), "foo/host", string(v)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := c.Put(context.TODO(), "foo/host2", string(v)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := GRPCResolver{c}
|
||||
|
||||
w, err := r.Resolve("foo")
|
||||
if err != nil {
|
||||
t.Fatal("failed to resolve foo", err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
updates, nerr := w.Next()
|
||||
if nerr != nil {
|
||||
t.Fatal(nerr)
|
||||
}
|
||||
if len(updates) != 2 {
|
||||
t.Fatalf("expected two updates, got %+v", updates)
|
||||
}
|
||||
|
||||
_, err = c.Txn(context.TODO()).Then(etcd.OpDelete("foo/host"), etcd.OpDelete("foo/host2")).Commit()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
updates, nerr = w.Next()
|
||||
if nerr != nil {
|
||||
t.Fatal(nerr)
|
||||
}
|
||||
if len(updates) != 2 || (updates[0].Op != naming.Delete && updates[1].Op != naming.Delete) {
|
||||
t.Fatalf("expected two updates, got %+v", updates)
|
||||
}
|
||||
}
|
150
clientv3/op.go
150
clientv3/op.go
@ -14,7 +14,9 @@
|
||||
|
||||
package clientv3
|
||||
|
||||
import pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
import (
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
)
|
||||
|
||||
type opType int
|
||||
|
||||
@ -41,10 +43,6 @@ type Op struct {
|
||||
serializable bool
|
||||
keysOnly bool
|
||||
countOnly bool
|
||||
minModRev int64
|
||||
maxModRev int64
|
||||
minCreateRev int64
|
||||
maxCreateRev int64
|
||||
|
||||
// for range, watch
|
||||
rev int64
|
||||
@ -54,45 +52,29 @@ type Op struct {
|
||||
|
||||
// progressNotify is for progress updates.
|
||||
progressNotify bool
|
||||
// createdNotify is for created event
|
||||
createdNotify bool
|
||||
// filters for watchers
|
||||
filterPut bool
|
||||
filterDelete bool
|
||||
|
||||
// for put
|
||||
val []byte
|
||||
leaseID LeaseID
|
||||
}
|
||||
|
||||
func (op Op) toRangeRequest() *pb.RangeRequest {
|
||||
if op.t != tRange {
|
||||
panic("op.t != tRange")
|
||||
}
|
||||
r := &pb.RangeRequest{
|
||||
Key: op.key,
|
||||
RangeEnd: op.end,
|
||||
Limit: op.limit,
|
||||
Revision: op.rev,
|
||||
Serializable: op.serializable,
|
||||
KeysOnly: op.keysOnly,
|
||||
CountOnly: op.countOnly,
|
||||
MinModRevision: op.minModRev,
|
||||
MaxModRevision: op.maxModRev,
|
||||
MinCreateRevision: op.minCreateRev,
|
||||
MaxCreateRevision: op.maxCreateRev,
|
||||
}
|
||||
if op.sort != nil {
|
||||
r.SortOrder = pb.RangeRequest_SortOrder(op.sort.Order)
|
||||
r.SortTarget = pb.RangeRequest_SortTarget(op.sort.Target)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (op Op) toRequestOp() *pb.RequestOp {
|
||||
switch op.t {
|
||||
case tRange:
|
||||
return &pb.RequestOp{Request: &pb.RequestOp_RequestRange{RequestRange: op.toRangeRequest()}}
|
||||
r := &pb.RangeRequest{
|
||||
Key: op.key,
|
||||
RangeEnd: op.end,
|
||||
Limit: op.limit,
|
||||
Revision: op.rev,
|
||||
Serializable: op.serializable,
|
||||
KeysOnly: op.keysOnly,
|
||||
CountOnly: op.countOnly,
|
||||
}
|
||||
if op.sort != nil {
|
||||
r.SortOrder = pb.RangeRequest_SortOrder(op.sort.Order)
|
||||
r.SortTarget = pb.RangeRequest_SortTarget(op.sort.Target)
|
||||
}
|
||||
return &pb.RequestOp{Request: &pb.RequestOp_RequestRange{RequestRange: r}}
|
||||
case tPut:
|
||||
r := &pb.PutRequest{Key: op.key, Value: op.val, Lease: int64(op.leaseID), PrevKv: op.prevKV}
|
||||
return &pb.RequestOp{Request: &pb.RequestOp_RequestPut{RequestPut: r}}
|
||||
@ -130,14 +112,6 @@ func OpDelete(key string, opts ...OpOption) Op {
|
||||
panic("unexpected serializable in delete")
|
||||
case ret.countOnly:
|
||||
panic("unexpected countOnly in delete")
|
||||
case ret.minModRev != 0, ret.maxModRev != 0:
|
||||
panic("unexpected mod revision filter in delete")
|
||||
case ret.minCreateRev != 0, ret.maxCreateRev != 0:
|
||||
panic("unexpected create revision filter in delete")
|
||||
case ret.filterDelete, ret.filterPut:
|
||||
panic("unexpected filter in delete")
|
||||
case ret.createdNotify:
|
||||
panic("unexpected createdNotify in delete")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@ -157,15 +131,7 @@ func OpPut(key, val string, opts ...OpOption) Op {
|
||||
case ret.serializable:
|
||||
panic("unexpected serializable in put")
|
||||
case ret.countOnly:
|
||||
panic("unexpected countOnly in put")
|
||||
case ret.minModRev != 0, ret.maxModRev != 0:
|
||||
panic("unexpected mod revision filter in put")
|
||||
case ret.minCreateRev != 0, ret.maxCreateRev != 0:
|
||||
panic("unexpected create revision filter in put")
|
||||
case ret.filterDelete, ret.filterPut:
|
||||
panic("unexpected filter in put")
|
||||
case ret.createdNotify:
|
||||
panic("unexpected createdNotify in put")
|
||||
panic("unexpected countOnly in delete")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@ -183,11 +149,7 @@ func opWatch(key string, opts ...OpOption) Op {
|
||||
case ret.serializable:
|
||||
panic("unexpected serializable in watch")
|
||||
case ret.countOnly:
|
||||
panic("unexpected countOnly in watch")
|
||||
case ret.minModRev != 0, ret.maxModRev != 0:
|
||||
panic("unexpected mod revision filter in watch")
|
||||
case ret.minCreateRev != 0, ret.maxCreateRev != 0:
|
||||
panic("unexpected create revision filter in watch")
|
||||
panic("unexpected countOnly in delete")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@ -219,14 +181,6 @@ func WithRev(rev int64) OpOption { return func(op *Op) { op.rev = rev } }
|
||||
// 'order' can be either 'SortNone', 'SortAscend', 'SortDescend'.
|
||||
func WithSort(target SortTarget, order SortOrder) OpOption {
|
||||
return func(op *Op) {
|
||||
if target == SortByKey && order == SortAscend {
|
||||
// If order != SortNone, server fetches the entire key-space,
|
||||
// and then applies the sort and limit, if provided.
|
||||
// Since current mvcc.Range implementation returns results
|
||||
// sorted by keys in lexicographically ascending order,
|
||||
// client should ignore SortOrder if the target is SortByKey.
|
||||
order = SortNone
|
||||
}
|
||||
op.sort = &SortOption{target, order}
|
||||
}
|
||||
}
|
||||
@ -261,15 +215,14 @@ func WithPrefix() OpOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRange specifies the range of 'Get', 'Delete', 'Watch' requests.
|
||||
// WithRange specifies the range of 'Get' or 'Delete' requests.
|
||||
// For example, 'Get' requests with 'WithRange(end)' returns
|
||||
// the keys in the range [key, end).
|
||||
// endKey must be lexicographically greater than start key.
|
||||
func WithRange(endKey string) OpOption {
|
||||
return func(op *Op) { op.end = []byte(endKey) }
|
||||
}
|
||||
|
||||
// WithFromKey specifies the range of 'Get', 'Delete', 'Watch' requests
|
||||
// WithFromKey specifies the range of 'Get' or 'Delete' requests
|
||||
// to be equal or greater than the key in the argument.
|
||||
func WithFromKey() OpOption { return WithRange("\x00") }
|
||||
|
||||
@ -291,18 +244,6 @@ func WithCountOnly() OpOption {
|
||||
return func(op *Op) { op.countOnly = true }
|
||||
}
|
||||
|
||||
// WithMinModRev filters out keys for Get with modification revisions less than the given revision.
|
||||
func WithMinModRev(rev int64) OpOption { return func(op *Op) { op.minModRev = rev } }
|
||||
|
||||
// WithMaxModRev filters out keys for Get with modification revisions greater than the given revision.
|
||||
func WithMaxModRev(rev int64) OpOption { return func(op *Op) { op.maxModRev = rev } }
|
||||
|
||||
// WithMinCreateRev filters out keys for Get with creation revisions less than the given revision.
|
||||
func WithMinCreateRev(rev int64) OpOption { return func(op *Op) { op.minCreateRev = rev } }
|
||||
|
||||
// WithMaxCreateRev filters out keys for Get with creation revisions greater than the given revision.
|
||||
func WithMaxCreateRev(rev int64) OpOption { return func(op *Op) { op.maxCreateRev = rev } }
|
||||
|
||||
// WithFirstCreate gets the key with the oldest creation revision in the request range.
|
||||
func WithFirstCreate() []OpOption { return withTop(SortByCreateRevision, SortAscend) }
|
||||
|
||||
@ -326,8 +267,7 @@ func withTop(target SortTarget, order SortOrder) []OpOption {
|
||||
return []OpOption{WithPrefix(), WithSort(target, order), WithLimit(1)}
|
||||
}
|
||||
|
||||
// WithProgressNotify makes watch server send periodic progress updates
|
||||
// every 10 minutes when there is no incoming events.
|
||||
// WithProgressNotify makes watch server send periodic progress updates.
|
||||
// Progress updates have zero events in WatchResponse.
|
||||
func WithProgressNotify() OpOption {
|
||||
return func(op *Op) {
|
||||
@ -335,23 +275,6 @@ func WithProgressNotify() OpOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithCreatedNotify makes watch server sends the created event.
|
||||
func WithCreatedNotify() OpOption {
|
||||
return func(op *Op) {
|
||||
op.createdNotify = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithFilterPut discards PUT events from the watcher.
|
||||
func WithFilterPut() OpOption {
|
||||
return func(op *Op) { op.filterPut = true }
|
||||
}
|
||||
|
||||
// WithFilterDelete discards DELETE events from the watcher.
|
||||
func WithFilterDelete() OpOption {
|
||||
return func(op *Op) { op.filterDelete = true }
|
||||
}
|
||||
|
||||
// WithPrevKV gets the previous key-value pair before the event happens. If the previous KV is already compacted,
|
||||
// nothing will be returned.
|
||||
func WithPrevKV() OpOption {
|
||||
@ -359,32 +282,3 @@ func WithPrevKV() OpOption {
|
||||
op.prevKV = true
|
||||
}
|
||||
}
|
||||
|
||||
// LeaseOp represents an Operation that lease can execute.
|
||||
type LeaseOp struct {
|
||||
id LeaseID
|
||||
|
||||
// for TimeToLive
|
||||
attachedKeys bool
|
||||
}
|
||||
|
||||
// LeaseOption configures lease operations.
|
||||
type LeaseOption func(*LeaseOp)
|
||||
|
||||
func (op *LeaseOp) applyOpts(opts []LeaseOption) {
|
||||
for _, opt := range opts {
|
||||
opt(op)
|
||||
}
|
||||
}
|
||||
|
||||
// WithAttachedKeys requests lease timetolive API to return
|
||||
// attached keys of given lease ID.
|
||||
func WithAttachedKeys() LeaseOption {
|
||||
return func(op *LeaseOp) { op.attachedKeys = true }
|
||||
}
|
||||
|
||||
func toLeaseTimeToLiveRequest(id LeaseID, opts ...LeaseOption) *pb.LeaseTimeToLiveRequest {
|
||||
ret := &LeaseOp{id: id}
|
||||
ret.applyOpts(opts)
|
||||
return &pb.LeaseTimeToLiveRequest{ID: int64(id), Keys: ret.attachedKeys}
|
||||
}
|
||||
|
@ -1,38 +0,0 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clientv3
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
)
|
||||
|
||||
// TestOpWithSort tests if WithSort(ASCEND, KEY) and WithLimit are specified,
|
||||
// RangeRequest ignores the SortOption to avoid unnecessarily fetching
|
||||
// the entire key-space.
|
||||
func TestOpWithSort(t *testing.T) {
|
||||
opReq := OpGet("foo", WithSort(SortByKey, SortAscend), WithLimit(10)).toRequestOp().Request
|
||||
q, ok := opReq.(*pb.RequestOp_RequestRange)
|
||||
if !ok {
|
||||
t.Fatalf("expected range request, got %v", reflect.TypeOf(opReq))
|
||||
}
|
||||
req := q.RequestRange
|
||||
wreq := &pb.RangeRequest{Key: []byte("foo"), SortOrder: pb.RangeRequest_NONE, Limit: 10}
|
||||
if !reflect.DeepEqual(req, wreq) {
|
||||
t.Fatalf("expected %+v, got %+v", wreq, req)
|
||||
}
|
||||
}
|
@ -15,117 +15,68 @@
|
||||
package clientv3
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
type rpcFunc func(ctx context.Context) error
|
||||
type retryRpcFunc func(context.Context, rpcFunc) error
|
||||
type retryRpcFunc func(context.Context, rpcFunc)
|
||||
|
||||
func (c *Client) newRetryWrapper() retryRpcFunc {
|
||||
return func(rpcCtx context.Context, f rpcFunc) error {
|
||||
return func(rpcCtx context.Context, f rpcFunc) {
|
||||
for {
|
||||
err := f(rpcCtx)
|
||||
if err == nil {
|
||||
return nil
|
||||
// ignore grpc conn closing on fail-fast calls; they are transient errors
|
||||
if err == nil || !isConnClosing(err) {
|
||||
return
|
||||
}
|
||||
|
||||
eErr := rpctypes.Error(err)
|
||||
// always stop retry on etcd errors
|
||||
if _, ok := eErr.(rpctypes.EtcdError); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// only retry if unavailable
|
||||
if grpc.Code(err) != codes.Unavailable {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.balancer.ConnectNotify():
|
||||
case <-rpcCtx.Done():
|
||||
return rpcCtx.Err()
|
||||
case <-c.ctx.Done():
|
||||
return c.ctx.Err()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) newAuthRetryWrapper() retryRpcFunc {
|
||||
return func(rpcCtx context.Context, f rpcFunc) error {
|
||||
for {
|
||||
err := f(rpcCtx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// always stop retry on etcd errors other than invalid auth token
|
||||
if rpctypes.Error(err) == rpctypes.ErrInvalidAuthToken {
|
||||
gterr := c.getToken(rpcCtx)
|
||||
if gterr != nil {
|
||||
return err // return the original error for simplicity
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RetryKVClient implements a KVClient that uses the client's FailFast retry policy.
|
||||
func RetryKVClient(c *Client) pb.KVClient {
|
||||
retryWrite := &retryWriteKVClient{pb.NewKVClient(c.conn), c.retryWrapper}
|
||||
return &retryKVClient{&retryWriteKVClient{retryWrite, c.retryAuthWrapper}}
|
||||
}
|
||||
|
||||
type retryKVClient struct {
|
||||
*retryWriteKVClient
|
||||
}
|
||||
|
||||
func (rkv *retryKVClient) Range(ctx context.Context, in *pb.RangeRequest, opts ...grpc.CallOption) (resp *pb.RangeResponse, err error) {
|
||||
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rkv.retryWriteKVClient.Range(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
type retryWriteKVClient struct {
|
||||
pb.KVClient
|
||||
retryf retryRpcFunc
|
||||
}
|
||||
|
||||
func (rkv *retryWriteKVClient) Put(ctx context.Context, in *pb.PutRequest, opts ...grpc.CallOption) (resp *pb.PutResponse, err error) {
|
||||
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
// RetryKVClient implements a KVClient that uses the client's FailFast retry policy.
|
||||
func RetryKVClient(c *Client) pb.KVClient {
|
||||
return &retryKVClient{pb.NewKVClient(c.conn), c.retryWrapper}
|
||||
}
|
||||
|
||||
func (rkv *retryKVClient) Put(ctx context.Context, in *pb.PutRequest, opts ...grpc.CallOption) (resp *pb.PutResponse, err error) {
|
||||
rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rkv.KVClient.Put(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (rkv *retryWriteKVClient) DeleteRange(ctx context.Context, in *pb.DeleteRangeRequest, opts ...grpc.CallOption) (resp *pb.DeleteRangeResponse, err error) {
|
||||
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
func (rkv *retryKVClient) DeleteRange(ctx context.Context, in *pb.DeleteRangeRequest, opts ...grpc.CallOption) (resp *pb.DeleteRangeResponse, err error) {
|
||||
rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rkv.KVClient.DeleteRange(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (rkv *retryWriteKVClient) Txn(ctx context.Context, in *pb.TxnRequest, opts ...grpc.CallOption) (resp *pb.TxnResponse, err error) {
|
||||
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
func (rkv *retryKVClient) Txn(ctx context.Context, in *pb.TxnRequest, opts ...grpc.CallOption) (resp *pb.TxnResponse, err error) {
|
||||
rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rkv.KVClient.Txn(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (rkv *retryWriteKVClient) Compact(ctx context.Context, in *pb.CompactionRequest, opts ...grpc.CallOption) (resp *pb.CompactionResponse, err error) {
|
||||
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
func (rkv *retryKVClient) Compact(ctx context.Context, in *pb.CompactionRequest, opts ...grpc.CallOption) (resp *pb.CompactionResponse, err error) {
|
||||
rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rkv.KVClient.Compact(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -139,12 +90,11 @@ type retryLeaseClient struct {
|
||||
|
||||
// RetryLeaseClient implements a LeaseClient that uses the client's FailFast retry policy.
|
||||
func RetryLeaseClient(c *Client) pb.LeaseClient {
|
||||
retry := &retryLeaseClient{pb.NewLeaseClient(c.conn), c.retryWrapper}
|
||||
return &retryLeaseClient{retry, c.retryAuthWrapper}
|
||||
return &retryLeaseClient{pb.NewLeaseClient(c.conn), c.retryWrapper}
|
||||
}
|
||||
|
||||
func (rlc *retryLeaseClient) LeaseGrant(ctx context.Context, in *pb.LeaseGrantRequest, opts ...grpc.CallOption) (resp *pb.LeaseGrantResponse, err error) {
|
||||
err = rlc.retryf(ctx, func(rctx context.Context) error {
|
||||
rlc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rlc.LeaseClient.LeaseGrant(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -153,7 +103,7 @@ func (rlc *retryLeaseClient) LeaseGrant(ctx context.Context, in *pb.LeaseGrantRe
|
||||
}
|
||||
|
||||
func (rlc *retryLeaseClient) LeaseRevoke(ctx context.Context, in *pb.LeaseRevokeRequest, opts ...grpc.CallOption) (resp *pb.LeaseRevokeResponse, err error) {
|
||||
err = rlc.retryf(ctx, func(rctx context.Context) error {
|
||||
rlc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rlc.LeaseClient.LeaseRevoke(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -171,7 +121,7 @@ func RetryClusterClient(c *Client) pb.ClusterClient {
|
||||
}
|
||||
|
||||
func (rcc *retryClusterClient) MemberAdd(ctx context.Context, in *pb.MemberAddRequest, opts ...grpc.CallOption) (resp *pb.MemberAddResponse, err error) {
|
||||
err = rcc.retryf(ctx, func(rctx context.Context) error {
|
||||
rcc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rcc.ClusterClient.MemberAdd(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -179,7 +129,7 @@ func (rcc *retryClusterClient) MemberAdd(ctx context.Context, in *pb.MemberAddRe
|
||||
}
|
||||
|
||||
func (rcc *retryClusterClient) MemberRemove(ctx context.Context, in *pb.MemberRemoveRequest, opts ...grpc.CallOption) (resp *pb.MemberRemoveResponse, err error) {
|
||||
err = rcc.retryf(ctx, func(rctx context.Context) error {
|
||||
rcc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rcc.ClusterClient.MemberRemove(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -187,7 +137,7 @@ func (rcc *retryClusterClient) MemberRemove(ctx context.Context, in *pb.MemberRe
|
||||
}
|
||||
|
||||
func (rcc *retryClusterClient) MemberUpdate(ctx context.Context, in *pb.MemberUpdateRequest, opts ...grpc.CallOption) (resp *pb.MemberUpdateResponse, err error) {
|
||||
err = rcc.retryf(ctx, func(rctx context.Context) error {
|
||||
rcc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rcc.ClusterClient.MemberUpdate(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -205,7 +155,7 @@ func RetryAuthClient(c *Client) pb.AuthClient {
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) AuthEnable(ctx context.Context, in *pb.AuthEnableRequest, opts ...grpc.CallOption) (resp *pb.AuthEnableResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.AuthEnable(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -213,7 +163,7 @@ func (rac *retryAuthClient) AuthEnable(ctx context.Context, in *pb.AuthEnableReq
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) AuthDisable(ctx context.Context, in *pb.AuthDisableRequest, opts ...grpc.CallOption) (resp *pb.AuthDisableResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.AuthDisable(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -221,7 +171,7 @@ func (rac *retryAuthClient) AuthDisable(ctx context.Context, in *pb.AuthDisableR
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserAdd(ctx context.Context, in *pb.AuthUserAddRequest, opts ...grpc.CallOption) (resp *pb.AuthUserAddResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.UserAdd(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -229,7 +179,7 @@ func (rac *retryAuthClient) UserAdd(ctx context.Context, in *pb.AuthUserAddReque
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserDelete(ctx context.Context, in *pb.AuthUserDeleteRequest, opts ...grpc.CallOption) (resp *pb.AuthUserDeleteResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.UserDelete(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -237,7 +187,7 @@ func (rac *retryAuthClient) UserDelete(ctx context.Context, in *pb.AuthUserDelet
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserChangePassword(ctx context.Context, in *pb.AuthUserChangePasswordRequest, opts ...grpc.CallOption) (resp *pb.AuthUserChangePasswordResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.UserChangePassword(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -245,7 +195,7 @@ func (rac *retryAuthClient) UserChangePassword(ctx context.Context, in *pb.AuthU
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserGrantRole(ctx context.Context, in *pb.AuthUserGrantRoleRequest, opts ...grpc.CallOption) (resp *pb.AuthUserGrantRoleResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.UserGrantRole(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -253,7 +203,7 @@ func (rac *retryAuthClient) UserGrantRole(ctx context.Context, in *pb.AuthUserGr
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserRevokeRole(ctx context.Context, in *pb.AuthUserRevokeRoleRequest, opts ...grpc.CallOption) (resp *pb.AuthUserRevokeRoleResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.UserRevokeRole(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -261,7 +211,7 @@ func (rac *retryAuthClient) UserRevokeRole(ctx context.Context, in *pb.AuthUserR
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) RoleAdd(ctx context.Context, in *pb.AuthRoleAddRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleAddResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.RoleAdd(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -269,7 +219,7 @@ func (rac *retryAuthClient) RoleAdd(ctx context.Context, in *pb.AuthRoleAddReque
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) RoleDelete(ctx context.Context, in *pb.AuthRoleDeleteRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleDeleteResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.RoleDelete(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -277,7 +227,7 @@ func (rac *retryAuthClient) RoleDelete(ctx context.Context, in *pb.AuthRoleDelet
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) RoleGrantPermission(ctx context.Context, in *pb.AuthRoleGrantPermissionRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleGrantPermissionResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.RoleGrantPermission(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
@ -285,7 +235,7 @@ func (rac *retryAuthClient) RoleGrantPermission(ctx context.Context, in *pb.Auth
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) RoleRevokePermission(ctx context.Context, in *pb.AuthRoleRevokePermissionRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleRevokePermissionResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.AuthClient.RoleRevokePermission(rctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
|
@ -19,7 +19,6 @@ import (
|
||||
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Txn is the interface that wraps mini-transactions.
|
||||
@ -153,12 +152,7 @@ func (txn *txn) Commit() (*TxnResponse, error) {
|
||||
|
||||
func (txn *txn) commit() (*TxnResponse, error) {
|
||||
r := &pb.TxnRequest{Compare: txn.cmps, Success: txn.sus, Failure: txn.fas}
|
||||
|
||||
var opts []grpc.CallOption
|
||||
if !txn.isWrite {
|
||||
opts = []grpc.CallOption{grpc.FailFast(false)}
|
||||
}
|
||||
resp, err := txn.kv.remote.Txn(txn.ctx, r, opts...)
|
||||
resp, err := txn.kv.remote.Txn(txn.ctx, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -17,13 +17,9 @@ package clientv3
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
)
|
||||
|
||||
func TestTxnPanics(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
kv := &kv{}
|
||||
|
||||
errc := make(chan string)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user