[WIP]Add tryLock REST API for distributed lock#1509
Conversation
| String lockPath = null; | ||
| if (_lockScope != null) { | ||
| lockPath = _lockScope.getPath(); | ||
| } | ||
|
|
||
| if (_lockPath != null) { | ||
| lockPath = _lockPath; | ||
| } |
There was a problem hiding this comment.
I can understand the logic. But it seemed not be a good readable code.
If we think lockPath is necessary:
if (_lockScope != null) {
_lockPath = _lockScope.getPath();
}
| private static final String ZK_ADDR = "zkAddress"; | ||
| private static final String LOCK_PATH = "lockPath"; | ||
| private static final String LOCK_TIMEOUT = "lockTimeout"; | ||
| private static final String USER_ID = "userId"; | ||
| private static final String LOCK_MSG = "lockMsg"; | ||
| private static final String LOCK_SCOPE_PROPERTY = "property"; | ||
| private static final String PATH_KEYS = "pathKeys"; | ||
| private static final String PATH_KEY_DELIMITER = "\\|"; |
There was a problem hiding this comment.
Let's create a constant class to hold them.
| @ResponseMetered(name = HttpConstants.WRITE_REQUEST) | ||
| @Timed(name = HttpConstants.WRITE_REQUEST) | ||
| @POST | ||
| @Path("lock") |
There was a problem hiding this comment.
Do you mean the URI "xxxx/locks/lock" ? Shall we call it "acquireLock"?
There was a problem hiding this comment.
Do you mean the URI "xxxx/locks/lock" ? Shall we call it "acquireLock"?
Write
In the REST API design, we don't expect a verb to represent a resource in the endpoint: the method POST/PUT/DELETE already tells the purpose.
@mgao0 Is this request "idempotent"? If one calls this endpoint N times with the same payload, will there be N locks, or just 1 lock?
| @POST | ||
| @Path("lock") | ||
| public Response tryLock(String content) { | ||
| System.out.println("Payload:\n" + content); |
| "Lock path is not found or lock scope could not be generated, please examine the request."); | ||
| } | ||
|
|
||
| return result ? OK(JSONRepresentation(lock)) : serverError(String.format( |
There was a problem hiding this comment.
You will not see the serverError part, right? Because it already returns badrequst in previous catch.
| LockInfo lockInfo; | ||
| try { | ||
| // Parse payload data | ||
| ZNRecord lockContent = toZNRecord(content); |
There was a problem hiding this comment.
It's not a good idea to let input content to be ZNRecord. User has to input "simpleFields, listFields, mapFields" even they dont use it. I would suggest to use plain json format.
| private String _userId; | ||
| private long _timeout; | ||
| private String _lockMsg; | ||
| private String _lockPath; |
There was a problem hiding this comment.
What if lockPath is confilct the lockPath in lockScope.
| ZooKeeperAccessor.class.getPackage().getName(), | ||
| MetadataStoreDirectoryAccessor.class.getPackage().getName() | ||
| }), | ||
| new String[]{AbstractHelixResource.class.getPackage().getName(), NamespacesAccessor.class |
There was a problem hiding this comment.
The format is a bit weird. Can you revert them to previous one?
| // Perform try lock | ||
| boolean result; | ||
| try { | ||
| result = lock.tryLock(); |
There was a problem hiding this comment.
This reminds me a question from our users. They wondered whether they can distinguish between zk error and lock owned by others. From our current implementation of zkclient, I think the answer is no, they are both returned as zero. could you double check that part? If that's the case, you may want to comment somewhere.
| } | ||
| } | ||
|
|
||
| @ResponseMetered(name = HttpConstants.WRITE_REQUEST) |
There was a problem hiding this comment.
To be consistent with Java API, this one should be tryLock.
Actually I suggest to think over all APIs before finalizing a specific one. E.g. for unlocking a lock or getting lock information, etc. I feel unlock should be similar to this, as an action, instead of DELETE operation. For get lock information, what information will you provide to users?
| if (znRecord == null) { | ||
| return null; | ||
| } | ||
| // Parse user iod |
| throw new Exception("Please check the timeout value input, could not convert to long."); | ||
| } | ||
|
|
||
| // Parse lock message |
There was a problem hiding this comment.
There are a couple comments like this, which I think the code is self explanatory. You may just remove them for concise code.
| pathKeys.add(participantName); | ||
| } | ||
| } | ||
| default: |
There was a problem hiding this comment.
Your previous try catch already excluded all other cases, so you don't need default here.
| String CLUSTER = "cluster"; | ||
| String RESOURCE = "resource"; | ||
| String PARTICIPANT = "participant"; |
There was a problem hiding this comment.
These should be defined as final static in the beginning too.
| private static final String LOCK_MSG = "lockMsg"; | ||
| private static final String LOCK_SCOPE_PROPERTY = "property"; | ||
| private static final String PATH_KEYS = "pathKeys"; | ||
| private static final String PATH_KEY_DELIMITER = "\\|"; |
There was a problem hiding this comment.
This definition doesn't look right. btw, it is not used anywhere.
| } | ||
|
|
||
| @Test(dependsOnMethods = "testLockAccessorWithHelixLockScope") | ||
| public void testLockAccessorWithUnknownScope() throws JsonProcessingException { |
There was a problem hiding this comment.
nit: maybe rename it to general scope etc.
| <artifact name="snakeyaml" m:classifier="sources" ext="jar"/> | ||
| </dependency> | ||
| <dependency org="org.apache.helix" name="helix-core" rev="1.0.2-SNAPSHOT" force="true" conf="compile->compile(*),master(*);runtime->runtime(*)"/> | ||
| <dependency org="org.apache.helix" name="helix-lock" rev="1.0.2-SNAPSHOT" force="true" conf="compile->compile(*),master(*);runtime->runtime(*)"/> |
| @Path("lock") | ||
| public Response tryLock(String content) { | ||
| System.out.println("Payload:\n" + content); | ||
| ZKDistributedNonblockingLock lock; |
There was a problem hiding this comment.
Nit: probably better if we declare it as an interface?
| // Build a lock object | ||
| lock = new ZKDistributedNonblockingLock.Builder().setLockMsg(lockInfo.getMessage()) | ||
| .setUserId(lockInfo.getOwner()).setTimeout(lockInfo.getTimeout()) | ||
| .setZkAddress(lockLocator.getZkAddress()).setLockPath(lockLocator.getLockPath()).build(); |
There was a problem hiding this comment.
Let's be careful here - what would the zk address be set as if the user was using it on multi-zk mode?
| .setUserId(lockInfo.getOwner()).setTimeout(lockInfo.getTimeout()) | ||
| .setZkAddress(lockLocator.getZkAddress()).setLockPath(lockLocator.getLockPath()).build(); | ||
| } catch (Exception e) { | ||
| return badRequest(e.getMessage()); |
There was a problem hiding this comment.
Should we log this as well?
| return badRequest( | ||
| "Lock path is not found or lock scope could not be generated, please examine the request."); |
There was a problem hiding this comment.
- We need to let the caller know what the request was. Consider embedding that information in the exception message.
- Should we add a server-side exception log here as well?
| if (znRecord == null) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
I'm not sure if this is a good pattern to use. Usually these functions expect a non-null values as parameters. Should you do a null check on the zn record before calling parseLockLocator()? This is a debatable point, but maybe worth giving some consideration.
| if (znRecord == null) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
same - should we do a null check on znrecord in the beginning?
| try { | ||
| lockTimeout = Long.parseLong(znRecord.getSimpleField(LOCK_TIMEOUT)); | ||
| } catch (NumberFormatException e) { | ||
| throw new Exception("Please check the timeout value input, could not convert to long."); |
There was a problem hiding this comment.
let's let the caller know what the value is?
| String CLUSTER = "cluster"; | ||
| String RESOURCE = "resource"; | ||
| String PARTICIPANT = "participant"; |
There was a problem hiding this comment.
It looks like we already have the enum for it. why can't we just use the enum instead of defining new static constants?
Either add a method in the enum that returns its lower-case equivalent or just do a enum.getString().toLowerCase().
| try { | ||
| property = HelixLockScope.LockScopeProperty.valueOf(lockScopeProperty); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new Exception(String.format( | ||
| "User requested lock scope property %s, but this property does not exist in HelixLockScope.", | ||
| lockScopeProperty)); | ||
| } |
There was a problem hiding this comment.
I think there's a shorter way of doing this. Try googling? It looks like Enums.getIfPresent(Blah.class, "A")...
| } | ||
| } | ||
| default: | ||
| // Not gonna happen |
There was a problem hiding this comment.
This doesn't look like a good example of a JavaDoc comment. Let's try to make it more descriptive or remove it altogether.
| } | ||
| if (pathKeys.isEmpty()) { | ||
| throw new Exception( | ||
| "Not enough path keys are provided to construct the lock path. Please check the request."); |
There was a problem hiding this comment.
Let the user know what the request was. Also do we need to log it on the server side?
|
Close due to inactive. |
Issues
(#200 - Link your issue number here: You can write "Fixes #XXX". Please use the proper keyword so that the issue gets closed automatically. See https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue
Any of the following keywords can be used: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved)
Description
(Write a concise description including what, why, how)
Tests
(List the names of added unit/integration tests)
(Before CI test pass, please copy & paste the result of "mvn test")
Documentation (Optional)
(Link the GitHub wiki you added)
Commits
Code Quality
(helix-style-intellij.xml if IntelliJ IDE is used)