-
Notifications
You must be signed in to change notification settings - Fork 0
HDDS-12356. OM Gatekeeper Prototype #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You 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 org.apache.hadoop.hdds.utils; | ||
|
|
||
| import java.util.Objects; | ||
| import org.jetbrains.annotations.NotNull; | ||
|
|
||
| /** | ||
| * This class provides specifications about a lock's requirements. | ||
| */ | ||
| public final class LockInfo implements Comparable<LockInfo> { | ||
| private final String key; | ||
| private final boolean isWriteLock; | ||
|
|
||
| private LockInfo(String key, boolean isWriteLock) { | ||
| this.key = key; | ||
| this.isWriteLock = isWriteLock; | ||
| } | ||
|
|
||
| public static LockInfo writeLockInfo(String key) { | ||
| return new LockInfo(key, true); | ||
| } | ||
|
|
||
| public static LockInfo readLockInfo(String key) { | ||
| return new LockInfo(key, false); | ||
| } | ||
|
|
||
| public String getKey() { | ||
| return key; | ||
| } | ||
|
|
||
| public boolean isWriteLock() { | ||
| return isWriteLock; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (this == o) { | ||
| return true; | ||
| } | ||
| if (!(o instanceof LockInfo)) { | ||
| return false; | ||
| } | ||
| LockInfo lockInfo = (LockInfo) o; | ||
| return isWriteLock == lockInfo.isWriteLock && Objects.equals(key, lockInfo.key); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hash(key, isWriteLock); | ||
| } | ||
|
|
||
| @Override | ||
| public int compareTo(@NotNull LockInfo other) { | ||
| return Integer.compare(hashCode(), other.hashCode()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sort by hashCode won't work with guava
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You 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 org.apache.hadoop.ozone.om.lock; | ||
|
|
||
| import java.util.HashSet; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
| import org.apache.hadoop.hdds.utils.LockInfo; | ||
| import org.apache.hadoop.ozone.OzoneConsts; | ||
|
|
||
| /** | ||
| * This class represents the locking information that is required by an OM request. | ||
| * Requests can define whether they need volume, bucket, or key locks of read or write types. | ||
| * A request gatekeeper can handle these requirements to identify conflicting requests. | ||
| * | ||
| * Sample usage: | ||
| * {@link org.apache.hadoop.ozone.om.request.OMClientRequest} adds a required method {@code getLockInfo} which returns | ||
| * an instance of this class. All OM requests then implement this method and return an instance of this class to define | ||
| * their locking requirements. | ||
| * | ||
| * Note that this class defines the interface for each of the ~100 OM requests to communicate their requirements to the | ||
| * gatekeeper. Any breaking changes to this interface will have a large ripple effect. | ||
| */ | ||
| public final class OmLockInfo { | ||
| private final LockInfo volumeLock; | ||
| private final LockInfo bucketLock; | ||
| private final Set<LockInfo> keyLocks; | ||
|
Comment on lines
+40
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we require locking on two or more volumes/buckets later on, it will require a lot of code change. |
||
|
|
||
| private OmLockInfo(Builder builder) { | ||
| volumeLock = builder.volumeLock; | ||
| bucketLock = builder.bucketLock; | ||
| keyLocks = builder.keyLocks; | ||
| } | ||
|
|
||
| public Optional<LockInfo> getVolumeLock() { | ||
| return Optional.ofNullable(volumeLock); | ||
| } | ||
|
|
||
| public Optional<LockInfo> getBucketLock() { | ||
| return Optional.ofNullable(bucketLock); | ||
| } | ||
|
|
||
| public Optional<Set<LockInfo>> getKeyLocks() { | ||
| return Optional.ofNullable(keyLocks); | ||
| } | ||
|
|
||
| /** | ||
| * Builds an {@link OmLockInfo} object with optional volume, bucket or key locks. | ||
| */ | ||
| public static final class Builder { | ||
| private LockInfo volumeLock; | ||
| private LockInfo bucketLock; | ||
| private Set<LockInfo> keyLocks; | ||
|
|
||
| public Builder() { | ||
| } | ||
|
|
||
| public void addVolumeReadLock(String volume) { | ||
| volumeLock = LockInfo.writeLockInfo(volume); | ||
| } | ||
|
|
||
| public void addVolumeWriteLock(String volume) { | ||
| volumeLock = LockInfo.readLockInfo(volume); | ||
| } | ||
|
|
||
| public void addBucketReadLock(String volume, String bucket) { | ||
| bucketLock = LockInfo.readLockInfo(joinStrings(volume, bucket)); | ||
| } | ||
|
|
||
| public void addBucketWriteLock(String volume, String bucket) { | ||
| bucketLock = LockInfo.writeLockInfo(joinStrings(volume, bucket)); | ||
| } | ||
|
|
||
| // Currently there is no use case for key level read locks. | ||
| public void addKeyWriteLock(String volume, String bucket, String key) { | ||
| // Lazy init keys. | ||
| if (keyLocks == null) { | ||
| keyLocks = new HashSet<>(); | ||
| } | ||
| keyLocks.add(LockInfo.writeLockInfo(joinStrings(volume, bucket, key))); | ||
| } | ||
|
|
||
| private String joinStrings(String... parts) { | ||
| return String.join(OzoneConsts.OZONE_URI_DELIMITER, parts); | ||
| } | ||
|
|
||
| public OmLockInfo build() { | ||
| return new OmLockInfo(this); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You 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 org.apache.hadoop.ozone.om.lock; | ||
|
|
||
| import com.google.common.util.concurrent.Striped; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.ListIterator; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.TimeoutException; | ||
| import java.util.concurrent.locks.Lock; | ||
| import java.util.concurrent.locks.ReadWriteLock; | ||
| import org.apache.hadoop.hdds.utils.LockInfo; | ||
|
|
||
| /** | ||
| * This class allows acquiring and releasing locks defined in an {@link OmLockInfo} instance. | ||
| * This can be used to allow non-conflicting OM requests to be executed in parallel. | ||
| * | ||
| * Sample Usage: | ||
| * {@link org.apache.hadoop.ozone.om.execution.OMExecutionFlow} instantiates this class and calls {@code lock} with the | ||
| * {@link OmLockInfo} of each request it is processing. | ||
| * | ||
| * Note that this class only provides one public method and will only be consumed by the OMExecutionFlow. Although this | ||
| * implementation uses 3 bins of striped locks, We can change the lock management implementation later as needed with | ||
| * little to no effect on any other classes. | ||
| */ | ||
| public class OmRequestGatekeeper { | ||
| private final Striped<ReadWriteLock> volumeLocks; | ||
| private final Striped<ReadWriteLock> bucketLocks; | ||
| // Currently only key level write locks are required. This lets us use the bulkGet API from the Striped class. | ||
| private final Striped<Lock> keyLocks; | ||
| private final long timeoutMillis; | ||
|
|
||
| // TODO Arbitrary values for now. | ||
| private static final int NUM_VOLUME_STRIPES = 128; | ||
| private static final int NUM_BUCKET_STRIPES = 1024; | ||
| private static final int NUM_KEY_STRIPES = 4096; | ||
|
|
||
| // If stats about the locks need to be added, they should be tracked here in the gatekeeper. | ||
|
|
||
| public OmRequestGatekeeper(long timeoutMillis) { | ||
| this.timeoutMillis = timeoutMillis; | ||
| volumeLocks = Striped.readWriteLock(NUM_VOLUME_STRIPES); | ||
| bucketLocks = Striped.readWriteLock(NUM_BUCKET_STRIPES); | ||
| keyLocks = Striped.lock(NUM_KEY_STRIPES); | ||
| } | ||
|
|
||
| /** | ||
| * Acquires all the locks specified by the {@code lockInfo} parameter. Caller is responsible for releasing the locks | ||
| * by closing the returned object. If an exception is thrown, no locks will be acquired when the method exits. | ||
| * @param lockInfo Defines the locks to acquire. | ||
| * @return A handle that can be closed to release the acquired locks. | ||
| * @throws InterruptedException If the thread is interrupted while waiting to acquire some of the locks. | ||
| * @throws TimeoutException If the timeout elapses while waiting to acquire any of the locks individually. | ||
| */ | ||
| public AutoCloseable lock(OmLockInfo lockInfo) throws InterruptedException, TimeoutException { | ||
| // Common case is 1 volume, 1 bucket, and at most 2 key locks. | ||
| List<Lock> locks = new ArrayList<>(4); | ||
|
|
||
| Optional<LockInfo> optionalVolumeLock = lockInfo.getVolumeLock(); | ||
| Optional<LockInfo> optionalBucketLock = lockInfo.getBucketLock(); | ||
| Optional<Set<LockInfo>> optionalKeyLocks = lockInfo.getKeyLocks(); | ||
|
|
||
| if (optionalVolumeLock.isPresent()) { | ||
| LockInfo volumeLockInfo = optionalVolumeLock.get(); | ||
| if (volumeLockInfo.isWriteLock()) { | ||
| locks.add(volumeLocks.get(volumeLockInfo.getKey()).writeLock()); | ||
| } else { | ||
| locks.add(volumeLocks.get(volumeLockInfo.getKey()).readLock()); | ||
| } | ||
| } | ||
|
|
||
| if (optionalBucketLock.isPresent()) { | ||
| LockInfo bucketLockInfo = optionalBucketLock.get(); | ||
| if (bucketLockInfo.isWriteLock()) { | ||
| locks.add(bucketLocks.get(bucketLockInfo.getKey()).writeLock()); | ||
| } else { | ||
| locks.add(bucketLocks.get(bucketLockInfo.getKey()).readLock()); | ||
| } | ||
| } | ||
|
|
||
| if (optionalKeyLocks.isPresent()) { | ||
| for (Lock keyLock: keyLocks.bulkGet(optionalKeyLocks.get())) { | ||
| locks.add(keyLock); | ||
| } | ||
| } | ||
|
|
||
| acquireLocks(locks); | ||
| return () -> releaseLocks(locks); | ||
| } | ||
|
Comment on lines
+73
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The lock overhead probably is quite high in this method. |
||
|
|
||
| /* | ||
| Optional: If we want more diagnostic info on the type of lock that failed to be acquired (volume, bucket, or key), | ||
| We can make the parameter a list of objects that wrap the Lock with information about its type. | ||
|
|
||
| Note that logging the specific volume, bucket or keys this lock was trying to acquire is not helpful and | ||
| misleading because collisions within the stripe lock might mean we are blocked on a request for a completely | ||
| different part of the namespace. | ||
| Obtaining the thread ID that we were waiting on would be more useful, but there is no easy way to do that. | ||
| */ | ||
| private void acquireLocks(List<Lock> locks) throws TimeoutException, InterruptedException { | ||
| List<Lock> acquiredLocks = new ArrayList<>(locks.size()); | ||
| for (Lock lock: locks) { | ||
| if (lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS)) { | ||
| try { | ||
| acquiredLocks.add(lock); | ||
| } catch (Throwable e) { | ||
| // We acquired this lock but were unable to add it to our acquired locks list. | ||
| lock.unlock(); | ||
| releaseLocks(acquiredLocks); | ||
| throw e; | ||
| } | ||
| } else { | ||
| releaseLocks(acquiredLocks); | ||
| throw new TimeoutException("Failed to acquire lock after the given timeout."); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+118
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Concurrently, if the |
||
|
|
||
| private void releaseLocks(List<Lock> locks) { | ||
| ListIterator<Lock> reverseIterator = locks.listIterator(); | ||
| while (reverseIterator.hasPrevious()) { | ||
| Lock lock = reverseIterator.previous(); | ||
| lock.unlock(); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use an enum is better than a boolean; see "Never Use Booleans for Something That Has Two States Now, but Might Have More Later".