Skip to content

Commit f3f3c1b

Browse files
darikoRevolution1
authored andcommitted
Add support for lock service API (#50)
* Add support for lock service API https://coreos.com/etcd/docs/latest/dev-guide/api_concurrency_reference_v3.html * add timeout to lock service API tests
1 parent aceada8 commit f3f3c1b

File tree

5 files changed

+199
-1
lines changed

5 files changed

+199
-1
lines changed

etcd3/apis/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .extra import ExtraAPI
66
from .kv import KVAPI
77
from .lease import LeaseAPI
8+
from .lock import LockAPI
89
from .maintenance import MaintenanceAPI
910
from .watch import WatchAPI
1011

@@ -17,4 +18,5 @@
1718
'MaintenanceAPI',
1819
'LeaseAPI',
1920
'BaseAPI'
21+
'LockAPI'
2022
]

etcd3/apis/lock.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from .base import BaseAPI
2+
3+
4+
class LockAPI(BaseAPI):
5+
def lock(self, name, lease=0):
6+
"""
7+
Lock acquires a distributed shared lock on a given named lock.
8+
On success, it will return a unique key that exists so long as
9+
the lock is held by the caller. This key can be used in
10+
conjunction with transactions to safely ensure updates to etcd
11+
only occur while holding lock ownership. The lock is held until
12+
Unlock is called on the key or the lease associate with the
13+
owner expires.
14+
15+
:type name: str
16+
:param name: name is the identifier for the distributed shared lock to be acquired.
17+
:type lease: int
18+
:param lease: lease is the lease ID to associate with the key in the key-value store. A lease
19+
value of 0 indicates no lease.
20+
"""
21+
22+
method = '/v3beta/lock/lock'
23+
data = {
24+
"name": name,
25+
"lease": lease
26+
}
27+
return self.call_rpc(method, data=data)
28+
29+
def unlock(self, key):
30+
"""
31+
Unlock takes a key returned by Lock and releases the hold on
32+
lock. The next Lock caller waiting for the lock will then be
33+
woken up and given ownership of the lock.
34+
35+
:type key: str
36+
:param key: key is the lock ownership key granted by Lock.
37+
:type lease: int
38+
:param lease: lease is the lease ID to associate with the key in the key-value store. A lease
39+
value of 0 indicates no lease.
40+
"""
41+
42+
method = '/v3beta/lock/unlock'
43+
data = {
44+
"key": key,
45+
}
46+
return self.call_rpc(method, data=data)

etcd3/baseclient.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .apis import ExtraAPI
1515
from .apis import KVAPI
1616
from .apis import LeaseAPI
17+
from .apis import LockAPI
1718
from .apis import MaintenanceAPI
1819
from .apis import WatchAPI
1920
from .stateful import Lease
@@ -46,7 +47,8 @@ def __iter__(self):
4647
raise NotImplementedError
4748

4849

49-
class BaseClient(AuthAPI, ClusterAPI, KVAPI, LeaseAPI, MaintenanceAPI, WatchAPI, ExtraAPI):
50+
class BaseClient(AuthAPI, ClusterAPI, KVAPI, LeaseAPI, MaintenanceAPI,
51+
WatchAPI, ExtraAPI, LockAPI):
5052
def __init__(self, host='localhost', port=2379, protocol='http',
5153
cert=(), verify=None,
5254
timeout=None, headers=None, user_agent=None, pool_size=30,

etcd3/rpc.swagger.json

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,60 @@
799799
]
800800
}
801801
},
802+
"/v3beta/lock/lock": {
803+
"post": {
804+
"summary": "Lock acquires a distributed shared lock on a given named lock.\nOn success, it will return a unique key that exists so long asnthe lock is held by the caller. This key can be used in\nconjunction with transactions to safely ensure updates to etcdnonly occur while holding lock ownership. The lock is held until\nUnlock is called on the key or the lease associate with thenowner expires.",
805+
"operationId": "Lock",
806+
"responses": {
807+
"200": {
808+
"description": "",
809+
"schema": {
810+
"$ref": "#/definitions/etcdserverpbLockResponse"
811+
}
812+
}
813+
},
814+
"parameters": [
815+
{
816+
"name": "body",
817+
"in": "body",
818+
"required": true,
819+
"schema": {
820+
"$ref": "#/definitions/etcdserverpbLockRequest"
821+
}
822+
}
823+
],
824+
"tags": [
825+
"Lock"
826+
]
827+
}
828+
},
829+
"/v3beta/lock/unlock": {
830+
"post": {
831+
"summary": "Unlock takes a key returned by Lock and releases the hold on\nlock. The next Lock caller waiting for the lock will then benwoken up and given ownership of the lock.",
832+
"operationId": "Unlock",
833+
"responses": {
834+
"200": {
835+
"description": "",
836+
"schema": {
837+
"$ref": "#/definitions/etcdserverpbUnlockResponse"
838+
}
839+
}
840+
},
841+
"parameters": [
842+
{
843+
"name": "body",
844+
"in": "body",
845+
"required": true,
846+
"schema": {
847+
"$ref": "#/definitions/etcdserverpbUnlockRequest"
848+
}
849+
}
850+
],
851+
"tags": [
852+
"Lock"
853+
]
854+
}
855+
},
802856
"/v3alpha/maintenance/alarm": {
803857
"post": {
804858
"summary": "Alarm activates, deactivates, and queries alarms regarding cluster health.",
@@ -1664,6 +1718,52 @@
16641718
}
16651719
}
16661720
},
1721+
"etcdserverpbLockRequest": {
1722+
"type": "object",
1723+
"properties": {
1724+
"name": {
1725+
"type": "string",
1726+
"format": "byte",
1727+
"description": "name is the identifier for the distributed shared lock to be acquired."
1728+
},
1729+
"lease": {
1730+
"type": "string",
1731+
"format": "int64",
1732+
"description": "lease is the ID of the lease that will be attached to ownership of the lock. If the lease expires or is revoked and currently holds the lock, the lock is automatically released. Calls to Lock with the same lease will be treated as a single acquisition; locking twice with the same lease is a no-op."
1733+
}
1734+
}
1735+
},
1736+
"etcdserverpbLockResponse": {
1737+
"type": "object",
1738+
"properties": {
1739+
"header": {
1740+
"$ref": "#/definitions/etcdserverpbResponseHeader"
1741+
},
1742+
"key": {
1743+
"type": "string",
1744+
"format": "byte",
1745+
"description": "key is a key that will exist on etcd for the duration that the Lock caller owns the lock. Users should not modify this key or the lock may exhibit undefined behavior."
1746+
}
1747+
}
1748+
},
1749+
"etcdserverpbUnlockRequest": {
1750+
"type": "object",
1751+
"properties": {
1752+
"key": {
1753+
"type": "string",
1754+
"format": "byte",
1755+
"description": "key is the lock ownership key granted by Lock."
1756+
}
1757+
}
1758+
},
1759+
"etcdserverpbUnlockResponse": {
1760+
"type": "object",
1761+
"properties": {
1762+
"header": {
1763+
"$ref": "#/definitions/etcdserverpbResponseHeader"
1764+
}
1765+
}
1766+
},
16671767
"etcdserverpbMember": {
16681768
"type": "object",
16691769
"properties": {

tests/test_lock_apis.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import time
2+
3+
import pytest
4+
5+
from etcd3.client import Client
6+
from tests.docker_cli import docker_run_etcd_main
7+
from .envs import protocol, host, port
8+
from .etcd_go_cli import NO_ETCD_SERVICE, etcdctl
9+
10+
11+
@pytest.fixture(scope='module')
12+
def client():
13+
"""
14+
init Etcd3Client, close its connection-pool when teardown
15+
"""
16+
_, p, _ = docker_run_etcd_main()
17+
c = Client(host, p, protocol)
18+
yield c
19+
c.close()
20+
21+
22+
@pytest.mark.skipif(NO_ETCD_SERVICE, reason="no etcd service available")
23+
def test_hash(client):
24+
assert client.hash().hash
25+
26+
@pytest.mark.timeout(60)
27+
@pytest.mark.skipif(NO_ETCD_SERVICE, reason="no etcd service available")
28+
def test_lock_flow(client):
29+
lease1 = client.Lease(5)
30+
lease1.grant()
31+
lock1 = client.lock('test_lock', lease1._ID)
32+
assert lock1.key.startswith(b'test_lock/')
33+
34+
lease2 = client.Lease(15)
35+
lease2.grant()
36+
start_lock_ts = time.time()
37+
client.lock('test_lock', lease2._ID)
38+
assert (time.time() - start_lock_ts) > 3
39+
40+
lease2.revoke()
41+
42+
lease3 = client.Lease(5)
43+
lease3.grant()
44+
start_lock_ts = time.time()
45+
lock3 = client.lock('test_lock', lease3._ID)
46+
assert (time.time() - start_lock_ts) < 2
47+
48+
client.unlock(lock3.key)

0 commit comments

Comments
 (0)