Skip to content

Use Opaque for locking instead of byte[], improve reuse of base64 keys #154

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

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

kohlschuetter
Copy link
Contributor

Related to discussion #149

The locking infrastructure currently exposes byte[] for keys, yet internally mostly uses base64-representations of that string.

Exposing byte[] is not recommended because the underlying bytes are not immutable, so they could be modified by any involved component after the fact. The base64 representations are currently computed on the fly, which may severely impact performance.

With this PR, we improve Opaque to be more immutable (while keeping backwards compatibility for now, marking unsafe methods deprecated for removal). We also cache the Opaque used from Inode, which further improves reuse.

The change is backwards compatible. We should however remove the deprecated methods as soon as we can.

Previously, we were creating Opaque objects multiple times for the same
file ID of an Inode.

Let Inode return the "opaque" key: introduce a "getFileIdKey" method
that always returns the same instance.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
We currently use byte[] in many places where we actually convert them
back into Base64 (and sometimes Base16) encoded strings, especially
around locking. This is suboptimal.  We also pass byte[] directly, which
allows direct manipulation, which is also not good.

Deprecate direct use of Inode.getFileId. Return an "Opaque" for locking
purposes as well (but allow them to be separate from the Opaque fileId
key to allow coarser locking).

Provide default implementations of the Opaque-variants in LockManager.
Changes to AbstractLockManager etc. follow in a separate commit.

Change the Base16-encoded file names in FsCache to the same
Base64-encoded locking key.

Make Opaque immutable-ish (deprecate constructor and getOpaque() method
that allows direct manipulation of the underlying byte[] but keep them
in for now for backwards compatibility), and add a cached
byte[]-to-Base64 string which is constructed only upon demand.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
Add "Opaque" methods to AbstractLockManager, which in turn call their
byte[]-counterpart.

Add a temporary, non-public AbstractLockManager2, which reverses this
logic.

Also add some simple logging for deprecated calls into
AbstractLockManager2 (which will be removed very soon).

Mark all byte[]-variants deprecated for removal.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
SimpleLm used to create new Base64-strings upon toKey(byte[]), which is
suboptimal.

Use Opaque.getBase64 instead.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
DistributedLockManager used to create new Base64-strings upon
objIdToKey(byte[]), which is suboptimal.

Use Opaque.getBase64 instead.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
This will change the test to use the new LockManager Opaque API.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
This will change the test to use the new LockManager Opaque API.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
Previously, we were using "new Opaque(bytes[])", which creates a new
Opaque instance backed by the given byte[].

Since the Opaque key may be used in a "owners" hashmap, make sure that
this information cannot be modified by the caller.

Use the immutable "Opaque.forBytes" instead.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
Copy link
Member

@kofemann kofemann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. The changes look good. I suggest going with a more aggressive approach and instead of deprication, removing old methods in the LockManager interface.

@@ -42,49 +43,116 @@ public abstract class AbstractLockManager implements LockManager {
* @param objId object id.
* @return exclusive lock.
*/
@Deprecated(forRemoval = true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have strong doubts that there are third-party implementations. I am for removing it right away.

Copy link
Contributor Author

@kohlschuetter kohlschuetter Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

Should we also remove the now-deprecated Opaque constructor and getOpaque method, or is that being used elsewhere? (including permanently serialized objects, for example)

Ideally, I would want to make Opaque an interface with a default implementation.
For example, in a future environment without PseudoFs involvement we would have Inode implement Opaque directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(see new commits below)

import org.dcache.nfs.util.Opaque;
import org.dcache.nfs.v4.xdr.nfs4_prot;

abstract class AbstractLockManager2 extends AbstractLockManager {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the added value of this class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backwards-compatibility only (for third-party LockManagers).
No longer necessary once we remove the deprecated methods (hence package-level protected)

}

public byte[] toNfsHandle() {
return nfsHandle.clone();
}

private byte[] buildNfsHandle() {
private byte[] buildNfsHandle(byte[] fs_opaque) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses instance attributes (but uses the passed fs_opaque byte[] directly because Opaque would unnecessarily clone() this).

Remove the now-deprecated byte[]-specific lock methods in favor of using
Opaque.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
This make Opaque completely immutable.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
This decouples the last bit of the remaining byte[] logic from Opaque.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
... and provide a default implementation inline.

This enables certain objects to function as Opaque keys directly.

Serializability is not required by the current code, so let's remove it.
If required, a new subclass can add support for it.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
Converting an Opaque to a byte[] or Base64-string may involve an
allocation, not just a mere cast/dereference.

Therefore, call these methods "toBytes" and toBase64".

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
@kohlschuetter kohlschuetter requested a review from kofemann June 27, 2025 10:58
@kohlschuetter
Copy link
Contributor Author

Hang on, a few more changes (low-hanging fruit) coming...

We can construct an Opaque instance from the bytes provided in a
ByteBuffer. This saves us one clone() in Inode.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
Clarify that this method creates a copy of the specified bytes.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
@kohlschuetter
Copy link
Contributor Author

@kofemann Please review again, thanks!

Removing the relative offset (which in our case is always 0) will reduce
the chance that somebody thinks it's an absolute offset from the
beginning of the ByteBuffer...

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
* @param length The number of bytes.
* @return The {@link Opaque} instance.
*/
public static Opaque forBytes(ByteBuffer buf, int length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ByteBuffer has the remaining method. Should do the job. No need to pass a length.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but we may not want to read the entire remainder

if (o instanceof OpaqueImpl) {
return Arrays.equals(_opaque, ((OpaqueImpl) o)._opaque);
} else {
return Arrays.equals(_opaque, ((Opaque) o).toBytes());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks the symmetry requirements:

if x.equals(y), then y.equals(x). Thus, o must be an instanceof OpaqueImpl.

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Object.html#equals(java.lang.Object)

Copy link
Contributor Author

@kohlschuetter kohlschuetter Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I follow.

This is the equals implementation for OpaqueImpl, so we know that about our own object.
We should be able to have "equals" be true for all implementations of Opaque (see line 96)
Then we special-case where both objects are OpaqueImpl -- here we can compare the two arrays directly without cloning (lines 100-101)
Lastly (line 102), we compare OpaqueImpl against any other Opaque implementation. Here we know that toBytes gives us the array to compare.

Other implementations of Opaque must behave the same way (i.e., first check instanceof Opaque, bail if not, then potentially optimize).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any particular reason for such implementation?

Apart from the points above: My goal is to have an environment where an Inode is the Opaque file id key.
That works once PseudoFS is out of the equation. (my custom VirtualFileSystem will return custom Inode subclasses that implement Opaque).

Since this may or may not be the default scenario for nfs4j, to keep the current Inode/file-id separation, we need a standard implementation (OpaqueImpl). However, hashCode and equals should ignore the specific implementation details because in the end only the bytes transferred from an NFS client matter (we don't want false-negative matches)

Copy link
Member

@kofemann kofemann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpaqueImpl#equals looks not compliant with Java spec. Is there any particular reason for such implementation?

Let's make sure subclasses get that right.

We cannot provide a "default" implementation for methods in
java.lang.Object, so let's provide static default helpers.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
@kohlschuetter kohlschuetter requested a review from kofemann June 27, 2025 15:25
... not required, since implicit in public interfaces.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
@kohlschuetter
Copy link
Contributor Author

one last improvement for this PR coming up in a sec...

... and add Opaque#numBytes and #putBytes.

This simplifies the dance between public-facing and inner Inode objects,
allowing Opaque objects to be reused between these.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
@kohlschuetter
Copy link
Contributor Author

kohlschuetter commented Jun 27, 2025

I assume you're not reviewing right now — I will add two more changes before I log off.

Now that we have a Opaque-specific constructor, let's use it instead of
converting back and forth from byte[].

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
When we try to resolve the inner FS's root inode given a pseudo inode in
PseudoFS, we can directly return the inner FS's root inode as returned
by VirtualFileSystem#getRootInode(), which is most likely cached,
instead of creating new Inode instances all over again.

This further reduces the number of Inode allocations.

Also fail with BadHandleException for any other pseudo inode because
they will by definition be invalid in the underlying file system.

Related: dCache#149
Signed-off-by: Christian Kohlschütter <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants