Skip to content

Commit b4335c1

Browse files
feat(lock): Implement the Lock API on HTTP (#478)
* WIP on Lock API Signed-off-by: Xavier Geerinck <[email protected]> * Add lock API Signed-off-by: Xavier Geerinck <[email protected]> * Remove unused variable Signed-off-by: Xavier Geerinck <[email protected]> * working on conflicts Signed-off-by: Xavier Geerinck <[email protected]> * Resolve issue Signed-off-by: Xavier Geerinck <[email protected]> * This should not have been committed Signed-off-by: Xavier Geerinck <[email protected]> --------- Signed-off-by: Xavier Geerinck <[email protected]>
1 parent 9fde09c commit b4335c1

File tree

11 files changed

+156
-96
lines changed

11 files changed

+156
-96
lines changed

daprdocs/content/en/js-sdk-docs/js-client/_index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,8 +471,8 @@ async function start() {
471471
let expiryInSeconds = 1000;
472472

473473
console.log(`Acquiring lock on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
474-
const tryLockResponse = await client.lock.tryLock(storeName, resourceId, lockOwner, expiryInSeconds);
475-
console.log(tryLockResponse);
474+
const lockResponse = await client.lock.lock(storeName, resourceId, lockOwner, expiryInSeconds);
475+
console.log(lockResponse);
476476

477477
console.log(`Unlocking on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
478478
const unlockResponse = await client.lock.unlock(storeName, resourceId, lockOwner);

examples/distributedLock/README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This example demonstrates 2 features (Try Lock & Unlock) from the [Distributed l
44

55
It demonstrates the **Distributed Lock** API's following methods:
66

7-
- `TryLock`
7+
- `Lock`
88
- `Unlock`
99

1010
> **Note:** Make sure to use the latest proto bindings by running scripts/fetch-proto.sh file.
@@ -17,12 +17,12 @@ It demonstrates the **Distributed Lock** API's following methods:
1717

1818
## Overview
1919

20-
The TryLock and Unlock calls are implemented under the client.lock attribute.
20+
The Lock and Unlock calls are implemented under the client.lock attribute.
2121

22-
#### TryLock Example
22+
#### Lock Example
2323

2424
```typescript
25-
const tryLockResponse = await client.lock.tryLock(storeName, resourceId, lockOwner, expiryInSeconds);
25+
const lockResponse = await client.lock.lock(storeName, resourceId, lockOwner, expiryInSeconds);
2626
```
2727

2828
#### Unlock Example
@@ -33,14 +33,14 @@ const unlockResponse = await client.lock.unlock(storeName, resourceId, lockOwner
3333

3434
### Start the Lock application.
3535

36-
Execute the example under the folder `examples/distributedLock/TryLockApplication`
36+
Execute the example under the folder `examples/distributedLock/LockApplication`
3737

3838
```bash
39-
cd examples/distributedLock/TryLockApplication
39+
cd examples/distributedLock/LockApplication
4040
npm install
4141
```
4242

43-
To run the `TryLock`, execute the following command:
43+
To run the `Lock`, execute the following command:
4444

4545
```bash
4646
dapr run --app-id lock --app-protocol grpc --components-path ./components npm run start
@@ -61,7 +61,7 @@ You should see the following output from the application:
6161

6262
### Start the Unlock Example.
6363

64-
Run `UnlockApplication` after `TryLockApplication` is ran.
64+
Run `UnlockApplication` after `LockApplication` is ran.
6565

6666
Navigate to examples/distributedLock/UnlockApplication.
6767

examples/distributedLock/TryLockApplication/src/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ async function start() {
3030
let expiryInSeconds = 1000;
3131

3232
console.log(`Acquiring lock on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
33-
const tryLockResponse = await client.lock.tryLock(storeName, resourceId, lockOwner, expiryInSeconds);
34-
console.log(tryLockResponse);
33+
const lockResponse = await client.lock.lock(storeName, resourceId, lockOwner, expiryInSeconds);
34+
console.log(lockResponse);
3535

3636
console.log(`Unlocking on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
3737
const unlockResponse = await client.lock.unlock(storeName, resourceId, lockOwner);
@@ -44,8 +44,8 @@ async function start() {
4444

4545
expiryInSeconds = 25;
4646
console.log(`Acquiring lock on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
47-
const tryLockResponse1 = await client.lock.tryLock(storeName, resourceId, lockOwner, expiryInSeconds);
48-
console.log("Acquired Lock? " + tryLockResponse1.success);
47+
const lockResponse1 = await client.lock.lock(storeName, resourceId, lockOwner, expiryInSeconds);
48+
console.log("Acquired Lock? " + lockResponse1.success);
4949

5050
await new Promise((resolve) => setTimeout(resolve, 20000));
5151
}

examples/distributedLock/UnLockApplication/src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ async function start() {
2929
const lockOwner = "owner2";
3030
const expiryInSeconds = 25;
3131

32-
// Trying to acquire the lock which is being used by the other process(TryLockApplication)
32+
// Trying to acquire the lock which is being used by the other process(LockApplication)
3333
console.log(`Acquiring lock on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
34-
let tryLockResponse = await client.lock.tryLock(storeName, resourceId, lockOwner, expiryInSeconds);
35-
console.log("Acquired Lock? " + tryLockResponse.success);
34+
let lockResponse = await client.lock.lock(storeName, resourceId, lockOwner, expiryInSeconds);
35+
console.log("Acquired Lock? " + lockResponse.success);
3636
console.log("Lock cannot be acquired as it belongs to the other process");
3737

3838
console.log(`Unlocking on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
@@ -45,8 +45,8 @@ async function start() {
4545

4646
// Trying to acquire the lock after the other process lock is expired
4747
console.log(`Acquiring lock on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
48-
tryLockResponse = await client.lock.tryLock(storeName, resourceId, lockOwner, expiryInSeconds);
49-
console.log("Acquired lock after the lock from the other process expired? " + tryLockResponse.success);
48+
lockResponse = await client.lock.lock(storeName, resourceId, lockOwner, expiryInSeconds);
49+
console.log("Acquired lock after the lock from the other process expired? " + lockResponse.success);
5050

5151
console.log(`Unlocking on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
5252
unLockResponse = await client.lock.unlock(storeName, resourceId, lockOwner);

src/implementation/Client/GRPCClient/lock.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ limitations under the License.
1212
*/
1313

1414
import GRPCClient from "./GRPCClient";
15-
import { TryLockResponse as TryLockResponseResult } from "../../../types/lock/TryLockResponse";
15+
import { LockResponse as LockResponseResult } from "../../../types/lock/LockResponse";
1616
import { UnlockResponse as UnLockResponseResult, LockStatus } from "../../../types/lock/UnlockResponse";
1717
import {
1818
TryLockRequest,
@@ -29,12 +29,12 @@ export default class GRPCClientLock implements IClientLock {
2929
this.client = client;
3030
}
3131

32-
async tryLock(
32+
async lock(
3333
storeName: string,
3434
resourceId: string,
3535
lockOwner: string,
3636
expiryInSeconds: number,
37-
): Promise<TryLockResponseResult> {
37+
): Promise<LockResponseResult> {
3838
const request = new TryLockRequest()
3939
.setStoreName(storeName)
4040
.setResourceId(resourceId)
@@ -48,7 +48,7 @@ export default class GRPCClientLock implements IClientLock {
4848
return reject(err);
4949
}
5050

51-
const wrapped: TryLockResponseResult = {
51+
const wrapped: LockResponseResult = {
5252
success: res.getSuccess(),
5353
};
5454

src/implementation/Client/HTTPClient/lock.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,62 @@ limitations under the License.
1212
*/
1313

1414
import HTTPClient from "./HTTPClient";
15-
import { TryLockResponse as TryLockResponseResult } from "../../../types/lock/TryLockResponse";
16-
import { UnlockResponse as UnLockResponseResult } from "../../../types/lock/UnlockResponse";
15+
import { LockResponse } from "../../../types/lock/LockResponse";
16+
import { UnlockResponse, LockStatus } from "../../../types/lock/UnlockResponse";
1717
import IClientLock from "../../../interfaces/Client/IClientLock";
18+
import { KeyValueType } from "../../../types/KeyValue.type";
1819

1920
export default class HTTPClientLock implements IClientLock {
2021
client: HTTPClient;
2122

2223
constructor(client: HTTPClient) {
2324
this.client = client;
2425
}
25-
tryLock(
26-
_storeName: string,
27-
_resourceId: string,
28-
_lockOwner: string,
29-
_expiryInSeconds: number,
30-
): Promise<TryLockResponseResult> {
31-
throw new Error("HTTP is currently not supported.");
26+
27+
async lock(storeName: string, resourceId: string, lockOwner: string, expiryInSeconds: number): Promise<LockResponse> {
28+
const data = {
29+
resourceId,
30+
lockOwner,
31+
expiryInSeconds,
32+
};
33+
34+
const result = await this.client.executeWithApiVersion("v1.0-alpha1", `/lock/${storeName}`, {
35+
method: "POST",
36+
body: data,
37+
});
38+
39+
return {
40+
success: (result as KeyValueType)["success"],
41+
};
42+
}
43+
44+
async unlock(storeName: string, resourceId: string, lockOwner: string): Promise<UnlockResponse> {
45+
const data = {
46+
resourceId,
47+
lockOwner,
48+
};
49+
50+
const result = await this.client.executeWithApiVersion("v1.0-alpha1", `/unlock/${storeName}`, {
51+
method: "POST",
52+
body: data,
53+
});
54+
55+
return {
56+
status: this._statusToLockStatus((result as KeyValueType)["status"]),
57+
};
3258
}
33-
unlock(_storeName: string, _resourceId: string, _lockOwner: string): Promise<UnLockResponseResult> {
34-
throw new Error("HTTP is currently not supported.");
59+
60+
_statusToLockStatus(status: number): LockStatus {
61+
switch (status) {
62+
case 0:
63+
return LockStatus.Success;
64+
case 1:
65+
return LockStatus.LockDoesNotExist;
66+
case 2:
67+
return LockStatus.LockBelongsToOthers;
68+
case 3:
69+
default:
70+
return LockStatus.InternalError;
71+
}
3572
}
3673
}

src/interfaces/Client/IClientLock.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ See the License for the specific language governing permissions and
1111
limitations under the License.
1212
*/
1313

14-
import { TryLockResponse } from "../../types/lock/TryLockResponse";
14+
import { LockResponse } from "../../types/lock/LockResponse";
1515
import { UnlockResponse } from "../../types/lock/UnlockResponse";
1616

1717
export default interface IClientLock {
@@ -22,7 +22,7 @@ export default interface IClientLock {
2222
* @param lockOwner owner owning the lock.
2323
* @param expiryInSeconds the expiry time for the lock.
2424
*/
25-
tryLock(storeName: string, resourceId: string, lockOwner: string, expiryInSeconds: number): Promise<TryLockResponse>;
25+
lock(storeName: string, resourceId: string, lockOwner: string, expiryInSeconds: number): Promise<LockResponse>;
2626

2727
/**
2828
*

src/types/lock/TryLockResponse.ts renamed to src/types/lock/LockResponse.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ See the License for the specific language governing permissions and
1111
limitations under the License.
1212
*/
1313

14-
export type TryLockResponse = {
14+
export type LockResponse = {
1515
success: boolean;
1616
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: dapr.io/v1alpha1
2+
kind: Component
3+
metadata:
4+
name: redislock
5+
spec:
6+
type: lock.redis
7+
version: v1
8+
metadata:
9+
- name: redisHost
10+
value: localhost:6379
11+
- name: redisPassword
12+
value: ""

test/e2e/common/client.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ See the License for the specific language governing permissions and
1111
limitations under the License.
1212
*/
1313

14+
import { randomUUID } from "crypto";
1415
import {
1516
CommunicationProtocolEnum,
1617
DaprClient,
1718
LogLevel,
1819
StateConcurrencyEnum,
1920
StateConsistencyEnum,
2021
} from "../../../src";
21-
import { describe, expect, it } from "@jest/globals";
2222
import { sleep } from "../../../src/utils/NodeJS.util";
23+
import { LockStatus } from "../../../src/types/lock/UnlockResponse";
2324

2425
const daprHost = "127.0.0.1";
2526
const daprGrpcPort = "50000";
@@ -150,6 +151,74 @@ describe("common/client", () => {
150151
});
151152
});
152153

154+
describe("distributed lock", () => {
155+
runIt("should be able to acquire a new lock and unlock", async (client: DaprClient) => {
156+
const resourceId = randomUUID();
157+
const lock = await client.lock.lock("redislock", resourceId, "owner1", 1000);
158+
expect(lock.success).toEqual(true);
159+
const unlock = await client.lock.unlock("redislock", resourceId, "owner1");
160+
expect(unlock.status).toEqual(LockStatus.Success);
161+
});
162+
163+
runIt("should be not be able to unlock when the lock is not acquired", async (client: DaprClient) => {
164+
const resourceId = randomUUID();
165+
const unlock = await client.lock.unlock("redislock", resourceId, "owner1");
166+
expect(unlock.status).toEqual(LockStatus.LockDoesNotExist);
167+
});
168+
169+
runIt("should be able to acquire a lock after the previous lock is expired", async (client: DaprClient) => {
170+
const resourceId = randomUUID();
171+
let lock = await client.lock.lock("redislock", resourceId, "owner1", 5);
172+
expect(lock.success).toEqual(true);
173+
await new Promise((resolve) => setTimeout(resolve, 2000));
174+
lock = await client.lock.lock("redislock", resourceId, "owner2", 5);
175+
expect(lock.success).toEqual(false);
176+
});
177+
178+
runIt(
179+
"should not be able to acquire a lock when the same lock is acquired by another owner",
180+
async (client: DaprClient) => {
181+
const resourceId = randomUUID();
182+
const lockOne = await client.lock.lock("redislock", resourceId, "owner1", 5);
183+
expect(lockOne.success).toEqual(true);
184+
const lockTwo = await client.lock.lock("redislock", resourceId, "owner2", 5);
185+
expect(lockTwo.success).toEqual(false);
186+
},
187+
);
188+
189+
runIt(
190+
"should be able to acquire a lock when a different lock is acquired by another owner",
191+
async (client: DaprClient) => {
192+
const lockOne = await client.lock.lock("redislock", randomUUID(), "owner1", 5);
193+
expect(lockOne.success).toEqual(true);
194+
const lockTwo = await client.lock.lock("redislock", randomUUID(), "owner2", 5);
195+
expect(lockTwo.success).toEqual(true);
196+
},
197+
);
198+
199+
runIt(
200+
"should not be able to acquire a lock when that lock is acquired by another owner/process",
201+
async (client: DaprClient) => {
202+
const resourceId = randomUUID();
203+
const lockOne = await client.lock.lock("redislock", resourceId, "owner3", 5);
204+
expect(lockOne.success).toEqual(true);
205+
const lockTwo = await client.lock.lock("redislock", resourceId, "owner4", 5);
206+
expect(lockTwo.success).toEqual(false);
207+
},
208+
);
209+
210+
runIt(
211+
"should not be able to unlock a lock when that lock is acquired by another owner/process",
212+
async (client: DaprClient) => {
213+
const resourceId = randomUUID();
214+
const lockOne = await client.lock.lock("redislock", resourceId, "owner5", 5);
215+
expect(lockOne.success).toEqual(true);
216+
const unlock = await client.lock.unlock("redislock", resourceId, "owner6");
217+
expect(unlock.status).toEqual(LockStatus.LockBelongsToOthers);
218+
},
219+
);
220+
});
221+
153222
describe("state", () => {
154223
const stateStoreName = "state-redis";
155224
const stateStoreMongoDbName = "state-mongodb";

0 commit comments

Comments
 (0)