diff --git a/fluss-filesystems/fluss-fs-juicefs/pom.xml b/fluss-filesystems/fluss-fs-juicefs/pom.xml new file mode 100644 index 00000000000..aebb49092b4 --- /dev/null +++ b/fluss-filesystems/fluss-fs-juicefs/pom.xml @@ -0,0 +1,193 @@ + + + + + 4.0.0 + + org.apache.fluss + fluss-filesystems + 1.0-SNAPSHOT + + + fluss-fs-juicefs + Fluss : FileSystems : JuiceFS + + + 1.4.0 + + + + + org.apache.fluss + fluss-common + ${project.version} + provided + + + + org.apache.fluss + fluss-fs-hadoop-shaded + ${project.version} + + + + org.apache.fluss + fluss-fs-hadoop + ${project.version} + + + + io.juicefs + juicefs-hadoop + ${fs.juicefs.sdk.version} + + + + org.apache.hadoop + hadoop-common + + + org.apache.hadoop + hadoop-client + + + + ch.qos.reload4j + reload4j + + + org.slf4j + slf4j-reload4j + + + org.slf4j + slf4j-log4j12 + + + log4j + log4j + + + commons-logging + commons-logging + + + + + + + org.apache.fluss + fluss-test-utils + + + org.apache.fluss + fluss-common + ${project.version} + test + test-jar + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + shade-fluss + package + + shade + + + + + *:* + + + javax.servlet:servlet-api + xmlenc:xmlenc + + + + + * + + .gitkeep + mime.types + mozilla/** + LICENSE.txt + license/LICENSE* + NOTICE + + + + org.apache.fluss:fluss-fs-hadoop + + META-INF/** + + + + + io.juicefs:juicefs-hadoop + + com/sun/jersey/** + META-INF/maven/com.sun.jersey/** + META-INF/taglib.tld + META-INF/LICENSE + META-INF/NOTICE.txt + META-INF/DEPENDENCIES + + + + + + org.apache.commons + org.apache.fluss.shaded.org.apache.commons + + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + + diff --git a/fluss-filesystems/fluss-fs-juicefs/src/main/java/org/apache/fluss/fs/juicefs/JuiceFsFileSystem.java b/fluss-filesystems/fluss-fs-juicefs/src/main/java/org/apache/fluss/fs/juicefs/JuiceFsFileSystem.java new file mode 100644 index 00000000000..0d138f1038d --- /dev/null +++ b/fluss-filesystems/fluss-fs-juicefs/src/main/java/org/apache/fluss/fs/juicefs/JuiceFsFileSystem.java @@ -0,0 +1,50 @@ +/* + * 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.fluss.fs.juicefs; + +import org.apache.fluss.fs.hdfs.HadoopFileSystem; +import org.apache.fluss.fs.token.ObtainedSecurityToken; + +import org.apache.hadoop.fs.FileSystem; + +import java.util.Collections; + +/** + * A {@link org.apache.fluss.fs.FileSystem} for JuiceFS that wraps a {@link HadoopFileSystem}. + * + *

Unlike the OSS / S3 plugins, JuiceFS does not require Fluss to obtain or distribute a + * delegation token: the JuiceFS client itself authenticates against the meta server using + * locally-configured credentials (e.g. {@code juicefs.access-key} / {@code juicefs.secret-key}, or + * implicit IAM). We therefore return an empty placeholder token from {@link + * #obtainSecurityToken()}. + */ +class JuiceFsFileSystem extends HadoopFileSystem { + + private static final ObtainedSecurityToken EMPTY_TOKEN = + new ObtainedSecurityToken( + JuiceFsPlugin.SCHEME, new byte[0], null, Collections.emptyMap()); + + JuiceFsFileSystem(FileSystem hadoopFileSystem) { + super(hadoopFileSystem); + } + + @Override + public ObtainedSecurityToken obtainSecurityToken() { + return EMPTY_TOKEN; + } +} diff --git a/fluss-filesystems/fluss-fs-juicefs/src/main/java/org/apache/fluss/fs/juicefs/JuiceFsPlugin.java b/fluss-filesystems/fluss-fs-juicefs/src/main/java/org/apache/fluss/fs/juicefs/JuiceFsPlugin.java new file mode 100644 index 00000000000..3aedb793d7d --- /dev/null +++ b/fluss-filesystems/fluss-fs-juicefs/src/main/java/org/apache/fluss/fs/juicefs/JuiceFsPlugin.java @@ -0,0 +1,147 @@ +/* + * 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.fluss.fs.juicefs; + +import org.apache.fluss.annotation.VisibleForTesting; +import org.apache.fluss.config.ConfigBuilder; +import org.apache.fluss.config.Configuration; +import org.apache.fluss.fs.FileSystem; +import org.apache.fluss.fs.FileSystemPlugin; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; + +/** + * Simple factory for the JuiceFS file system. + * + *

This plugin registers the {@code jfs} scheme and bridges Fluss to the JuiceFS Hadoop SDK + * ({@code io.juicefs.JuiceFileSystem}). The JuiceFS client itself manages authentication via its + * meta server / access keys, so no Fluss-side delegation token is required. + * + *

Configuration is propagated from Fluss to the underlying Hadoop {@link + * org.apache.hadoop.conf.Configuration} for any key starting with {@code fs.jfs.} or {@code + * juicefs.}. Two defaults are injected when not provided by the user: + * + *

+ */ +public class JuiceFsPlugin implements FileSystemPlugin { + + private static final Logger LOG = LoggerFactory.getLogger(JuiceFsPlugin.class); + + public static final String SCHEME = "jfs"; + + /** Fully qualified class name of the JuiceFS Hadoop SDK FileSystem implementation. */ + static final String JUICEFS_HADOOP_FS_IMPL = "io.juicefs.JuiceFileSystem"; + + /** Hadoop configuration key that selects the {@code jfs} scheme implementation. */ + static final String FS_JFS_IMPL_KEY = "fs.jfs.impl"; + + /** Hadoop configuration key controlling the FileSystem cache for the {@code jfs} scheme. */ + static final String FS_JFS_IMPL_DISABLE_CACHE_KEY = "fs.jfs.impl.disable.cache"; + + /** + * In order to simplify, fluss juicefs configuration keys mirror the upstream Hadoop / JuiceFS + * keys. Any Fluss config entry whose key starts with one of these prefixes is forwarded to the + * Hadoop configuration. + * + * + */ + private static final String[] FLUSS_CONFIG_PREFIXES = {"fs.jfs.", "juicefs."}; + + @Override + public String getScheme() { + return SCHEME; + } + + @Override + public FileSystem create(URI fsUri, Configuration flussConfig) throws IOException { + org.apache.hadoop.conf.Configuration hadoopConfig = getHadoopConfiguration(flussConfig); + applyJuiceFsDefaults(hadoopConfig); + + // handle missing scheme/authority by falling back to the configured default URI + final String scheme = fsUri.getScheme(); + final String authority = fsUri.getAuthority(); + if (scheme == null && authority == null) { + fsUri = org.apache.hadoop.fs.FileSystem.getDefaultUri(hadoopConfig); + } else if (scheme != null && authority == null) { + URI defaultUri = org.apache.hadoop.fs.FileSystem.getDefaultUri(hadoopConfig); + if (scheme.equals(defaultUri.getScheme()) && defaultUri.getAuthority() != null) { + fsUri = defaultUri; + } + } + + // load the JuiceFS Hadoop SDK reflectively via Hadoop's FileSystem.newInstance, so that + // the Fluss bytecode does not have a hard compile-time dependency on io.juicefs classes + org.apache.hadoop.fs.FileSystem hadoopFs = + org.apache.hadoop.fs.FileSystem.newInstance(fsUri, hadoopConfig); + LOG.info( + "Created JuiceFS Hadoop FileSystem: scheme={}, authority={}, impl={}", + fsUri.getScheme(), + fsUri.getAuthority(), + hadoopFs.getClass().getName()); + + return new JuiceFsFileSystem(hadoopFs); + } + + /** + * Inject sensible defaults for the JuiceFS Hadoop bridge if the user has not specified them. + * + *

This is package-private to allow direct testing without a live JuiceFS meta server. + */ + @VisibleForTesting + static void applyJuiceFsDefaults(org.apache.hadoop.conf.Configuration hadoopConfig) { + if (hadoopConfig.get(FS_JFS_IMPL_KEY) == null) { + hadoopConfig.set(FS_JFS_IMPL_KEY, JUICEFS_HADOOP_FS_IMPL); + } + if (hadoopConfig.get(FS_JFS_IMPL_DISABLE_CACHE_KEY) == null) { + hadoopConfig.set(FS_JFS_IMPL_DISABLE_CACHE_KEY, "false"); + } + } + + @VisibleForTesting + org.apache.hadoop.conf.Configuration getHadoopConfiguration(Configuration flussConfig) { + org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration(); + if (flussConfig == null) { + return conf; + } + + // read all configuration entries with a prefix in 'FLUSS_CONFIG_PREFIXES' + for (String key : flussConfig.keySet()) { + for (String prefix : FLUSS_CONFIG_PREFIXES) { + if (key.startsWith(prefix)) { + String value = + flussConfig.getString( + ConfigBuilder.key(key).stringType().noDefaultValue(), null); + conf.set(key, value); + LOG.debug("Adding Fluss config entry {} to Hadoop config", key); + break; + } + } + } + return conf; + } +} diff --git a/fluss-filesystems/fluss-fs-juicefs/src/main/java/org/apache/fluss/fs/juicefs/JuiceFsSecurityTokenReceiver.java b/fluss-filesystems/fluss-fs-juicefs/src/main/java/org/apache/fluss/fs/juicefs/JuiceFsSecurityTokenReceiver.java new file mode 100644 index 00000000000..79353bf9a46 --- /dev/null +++ b/fluss-filesystems/fluss-fs-juicefs/src/main/java/org/apache/fluss/fs/juicefs/JuiceFsSecurityTokenReceiver.java @@ -0,0 +1,53 @@ +/* + * 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.fluss.fs.juicefs; + +import org.apache.fluss.fs.token.ObtainedSecurityToken; +import org.apache.fluss.fs.token.SecurityTokenReceiver; + +/** + * Security token receiver for JuiceFS filesystems. + * + *

Unlike the OSS / S3 / COS plugins, Fluss does not perform any STS or delegation-token exchange + * for JuiceFS: the JuiceFS client itself authenticates locally against the metadata engine and the + * backing object storage. Consequently {@link + * org.apache.fluss.fs.juicefs.JuiceFsFileSystem#obtainSecurityToken()} returns an empty placeholder + * token with scheme {@code "jfs"}. + * + *

This receiver exists solely to satisfy the contract of {@code SecurityTokenReceiverRepository} + * on the client side. Without it, every placeholder token arriving with scheme {@code "jfs"} would + * be reported as {@code "Token arrived for service but no receiver found for it: jfs"}, which is + * caught inside {@code DefaultSecurityTokenManager} and translated into a periodic re-schedule of + * the token renewal task after {@code client.filesystem.security.token.renewal.backoff} — a + * persistent, unnecessary retry / log-noise loop on every JuiceFS-enabled client. + * + *

The implementation mirrors {@link org.apache.fluss.fs.hdfs.HdfsSecurityTokenReceiver} and is + * intentionally a no-op. + */ +public class JuiceFsSecurityTokenReceiver implements SecurityTokenReceiver { + + @Override + public String scheme() { + return JuiceFsPlugin.SCHEME; + } + + @Override + public void onNewTokensObtained(ObtainedSecurityToken token) { + // no-op: JuiceFS authenticates locally on each node, so there is nothing to install. + } +} diff --git a/fluss-filesystems/fluss-fs-juicefs/src/main/resources/META-INF/NOTICE b/fluss-filesystems/fluss-fs-juicefs/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..0e5dc95ca97 --- /dev/null +++ b/fluss-filesystems/fluss-fs-juicefs/src/main/resources/META-INF/NOTICE @@ -0,0 +1,59 @@ +fluss-fs-juicefs +Copyright 2025-2026 The Apache Software Foundation + +This project includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +This project bundles the following dependencies under the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt) + +- com.fasterxml.jackson.core:jackson-annotations:2.15.3 +- com.fasterxml.jackson.core:jackson-core:2.15.3 +- com.fasterxml.jackson.core:jackson-databind:2.15.3 +- com.fasterxml.woodstox:woodstox-core:5.4.0 +- com.google.guava:failureaccess:1.0 +- com.google.guava:guava:27.0-jre +- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava +- com.google.j2objc:j2objc-annotations:1.1 +- com.google.re2j:re2j:1.1 +- commons-beanutils:commons-beanutils:1.9.4 +- commons-collections:commons-collections:3.2.2 +- commons-io:commons-io:2.14.0 +- commons-logging:commons-logging:1.2 +- dnsjava:dnsjava:3.4.0 +- io.dropwizard.metrics:metrics-core:3.2.4 +- io.juicefs:juicefs-hadoop:1.4.0 +- io.netty:netty-buffer:4.1.100.Final +- io.netty:netty-codec:4.1.100.Final +- io.netty:netty-common:4.1.100.Final +- io.netty:netty-handler:4.1.100.Final +- io.netty:netty-resolver:4.1.100.Final +- io.netty:netty-transport-classes-epoll:4.1.100.Final +- io.netty:netty-transport-native-epoll:4.1.100.Final +- io.netty:netty-transport-native-unix-common:4.1.100.Final +- io.netty:netty-transport:4.1.100.Final +- jakarta.activation:jakarta.activation-api:1.2.1 +- org.apache.commons:commons-compress:1.24.0 +- org.apache.commons:commons-configuration2:2.8.0 +- org.apache.commons:commons-lang3:3.18.0 +- org.apache.commons:commons-text:1.10.0 +- org.apache.hadoop:hadoop-annotations:3.4.0 +- org.apache.hadoop:hadoop-auth:3.4.0 +- org.apache.hadoop:hadoop-common:3.4.0 +- org.apache.hadoop.thirdparty:hadoop-shaded-guava:1.2.0 +- org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_21:1.2.0 +- org.apache.kerby:kerb-core:2.0.3 +- org.apache.kerby:kerby-asn1:2.0.3 +- org.apache.kerby:kerby-pkix:2.0.3 +- org.apache.kerby:kerby-util:2.0.3 +- org.codehaus.jettison:jettison:1.5.4 +- org.codehaus.mojo:animal-sniffer-annotations:1.17 +- org.codehaus.woodstox:stax2-api:4.2.1 +- org.xerial.snappy:snappy-java:1.1.10.4 + +This project bundles the following dependencies under the Bouncy Castle License (https://www.bouncycastle.org/licence.html) + +- org.bouncycastle:bcprov-jdk15on:1.70 + +This project bundles the following dependencies under the MIT License (https://opensource.org/licenses/MIT) + +- org.checkerframework:checker-qual:2.5.2 diff --git a/fluss-filesystems/fluss-fs-juicefs/src/main/resources/META-INF/services/org.apache.fluss.fs.FileSystemPlugin b/fluss-filesystems/fluss-fs-juicefs/src/main/resources/META-INF/services/org.apache.fluss.fs.FileSystemPlugin new file mode 100644 index 00000000000..96de7aefe2a --- /dev/null +++ b/fluss-filesystems/fluss-fs-juicefs/src/main/resources/META-INF/services/org.apache.fluss.fs.FileSystemPlugin @@ -0,0 +1,17 @@ +# 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. + +org.apache.fluss.fs.juicefs.JuiceFsPlugin diff --git a/fluss-filesystems/fluss-fs-juicefs/src/main/resources/META-INF/services/org.apache.fluss.fs.token.SecurityTokenReceiver b/fluss-filesystems/fluss-fs-juicefs/src/main/resources/META-INF/services/org.apache.fluss.fs.token.SecurityTokenReceiver new file mode 100644 index 00000000000..5234992e28f --- /dev/null +++ b/fluss-filesystems/fluss-fs-juicefs/src/main/resources/META-INF/services/org.apache.fluss.fs.token.SecurityTokenReceiver @@ -0,0 +1,17 @@ +# 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. + +org.apache.fluss.fs.juicefs.JuiceFsSecurityTokenReceiver diff --git a/fluss-filesystems/fluss-fs-juicefs/src/test/java/org/apache/fluss/fs/juicefs/JuiceFsPluginTest.java b/fluss-filesystems/fluss-fs-juicefs/src/test/java/org/apache/fluss/fs/juicefs/JuiceFsPluginTest.java new file mode 100644 index 00000000000..04e0de4af20 --- /dev/null +++ b/fluss-filesystems/fluss-fs-juicefs/src/test/java/org/apache/fluss/fs/juicefs/JuiceFsPluginTest.java @@ -0,0 +1,83 @@ +/* + * 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.fluss.fs.juicefs; + +import org.apache.fluss.config.Configuration; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** Tests that validate the behavior of the JuiceFS File System Plugin. */ +class JuiceFsPluginTest { + + @Test + void testScheme() { + assertThat(new JuiceFsPlugin().getScheme()).isEqualTo("jfs"); + } + + @Test + void testHadoopConfigPropagation() { + Configuration flussConfig = new Configuration(); + flussConfig.setString("fs.jfs.access-key", "ak-value"); + flussConfig.setString("juicefs.meta", "redis://meta-host:6379/1"); + flussConfig.setString("juicefs.cache-dir", "/var/jfsCache"); + // unrelated key — should NOT be forwarded + flussConfig.setString("unrelated.key", "x"); + + org.apache.hadoop.conf.Configuration hadoopConfig = + new JuiceFsPlugin().getHadoopConfiguration(flussConfig); + + assertThat(hadoopConfig.get("fs.jfs.access-key")).isEqualTo("ak-value"); + assertThat(hadoopConfig.get("juicefs.meta")).isEqualTo("redis://meta-host:6379/1"); + assertThat(hadoopConfig.get("juicefs.cache-dir")).isEqualTo("/var/jfsCache"); + assertThat(hadoopConfig.get("unrelated.key")).isNull(); + } + + @Test + void testApplyJuiceFsDefaultsInjectsImpl() { + org.apache.hadoop.conf.Configuration hadoopConfig = + new org.apache.hadoop.conf.Configuration(false); + JuiceFsPlugin.applyJuiceFsDefaults(hadoopConfig); + + assertThat(hadoopConfig.get(JuiceFsPlugin.FS_JFS_IMPL_KEY)) + .isEqualTo(JuiceFsPlugin.JUICEFS_HADOOP_FS_IMPL); + assertThat(hadoopConfig.get(JuiceFsPlugin.FS_JFS_IMPL_DISABLE_CACHE_KEY)) + .isEqualTo("false"); + } + + @Test + void testApplyJuiceFsDefaultsDoesNotOverrideUserValues() { + org.apache.hadoop.conf.Configuration hadoopConfig = + new org.apache.hadoop.conf.Configuration(false); + hadoopConfig.set(JuiceFsPlugin.FS_JFS_IMPL_KEY, "com.foo.MyJuiceFs"); + hadoopConfig.set(JuiceFsPlugin.FS_JFS_IMPL_DISABLE_CACHE_KEY, "true"); + + JuiceFsPlugin.applyJuiceFsDefaults(hadoopConfig); + + assertThat(hadoopConfig.get(JuiceFsPlugin.FS_JFS_IMPL_KEY)).isEqualTo("com.foo.MyJuiceFs"); + assertThat(hadoopConfig.get(JuiceFsPlugin.FS_JFS_IMPL_DISABLE_CACHE_KEY)).isEqualTo("true"); + } + + @Test + void testGetHadoopConfigurationWithNullFlussConfig() { + org.apache.hadoop.conf.Configuration hadoopConfig = + new JuiceFsPlugin().getHadoopConfiguration(null); + assertThat(hadoopConfig).isNotNull(); + } +} diff --git a/fluss-filesystems/fluss-fs-juicefs/src/test/java/org/apache/fluss/fs/juicefs/JuiceFsSecurityTokenReceiverTest.java b/fluss-filesystems/fluss-fs-juicefs/src/test/java/org/apache/fluss/fs/juicefs/JuiceFsSecurityTokenReceiverTest.java new file mode 100644 index 00000000000..d84d886be12 --- /dev/null +++ b/fluss-filesystems/fluss-fs-juicefs/src/test/java/org/apache/fluss/fs/juicefs/JuiceFsSecurityTokenReceiverTest.java @@ -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.fluss.fs.juicefs; + +import org.apache.fluss.fs.token.ObtainedSecurityToken; +import org.apache.fluss.fs.token.SecurityTokenReceiver; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ServiceLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link JuiceFsSecurityTokenReceiver}. + * + *

These tests protect the client-side flow against the regression described in {@link + * JuiceFsSecurityTokenReceiver}: without a receiver registered for the {@code "jfs"} scheme, the + * placeholder token returned by {@link JuiceFsFileSystem#obtainSecurityToken()} triggers an {@code + * IllegalStateException} inside {@code SecurityTokenReceiverRepository}, which {@code + * DefaultSecurityTokenManager} translates into a periodic retry loop. + */ +class JuiceFsSecurityTokenReceiverTest { + + @Test + void schemeShouldBeJfs() { + assertThat(new JuiceFsSecurityTokenReceiver().scheme()).isEqualTo(JuiceFsPlugin.SCHEME); + } + + @Test + void onNewTokensObtainedShouldBeANoOpForPlaceholderToken() { + JuiceFsSecurityTokenReceiver receiver = new JuiceFsSecurityTokenReceiver(); + // Same shape as the token produced by JuiceFsFileSystem#obtainSecurityToken(). + ObtainedSecurityToken placeholder = + new ObtainedSecurityToken( + JuiceFsPlugin.SCHEME, new byte[0], null, Collections.emptyMap()); + + assertThatCode(() -> receiver.onNewTokensObtained(placeholder)).doesNotThrowAnyException(); + } + + @Test + void serviceLoaderShouldDiscoverJfsReceiver() { + // Mirrors the discovery contract used by SecurityTokenReceiverRepository#loadReceivers(). + List discovered = new ArrayList<>(); + ServiceLoader.load( + SecurityTokenReceiver.class, SecurityTokenReceiver.class.getClassLoader()) + .forEach(discovered::add); + + assertThat(discovered) + .extracting(SecurityTokenReceiver::scheme) + .contains(JuiceFsPlugin.SCHEME); + } +} diff --git a/fluss-filesystems/pom.xml b/fluss-filesystems/pom.xml index a47ff5a5029..60bd578099d 100644 --- a/fluss-filesystems/pom.xml +++ b/fluss-filesystems/pom.xml @@ -38,6 +38,7 @@ fluss-fs-obs fluss-fs-cos fluss-fs-hdfs + fluss-fs-juicefs pom diff --git a/fluss-test-coverage/pom.xml b/fluss-test-coverage/pom.xml index b742c2d8e1e..79ba43ed22b 100644 --- a/fluss-test-coverage/pom.xml +++ b/fluss-test-coverage/pom.xml @@ -467,6 +467,7 @@ org.apache.fluss.fs.cos.* org.apache.fluss.fs.s3.* org.apache.fluss.fs.obs.* + org.apache.fluss.fs.juicefs.* com.amazonaws.services.s3.model.transform.XmlResponsesSaxParser* org.apache.fluss.rocksdb.RocksIteratorWrapper diff --git a/website/docs/maintenance/tiered-storage/filesystems/juicefs.md b/website/docs/maintenance/tiered-storage/filesystems/juicefs.md new file mode 100644 index 00000000000..51fe16df371 --- /dev/null +++ b/website/docs/maintenance/tiered-storage/filesystems/juicefs.md @@ -0,0 +1,151 @@ +--- +title: JuiceFS +sidebar_position: 8 +--- + +# JuiceFS + +[JuiceFS](https://juicefs.com) is a distributed POSIX-compatible file system built on top of object storage and a separate metadata engine (Redis, TiKV, MySQL, PostgreSQL, etc.). It ships with a Hadoop-compatible Java SDK, which Fluss uses to store snapshots for Primary-Key Tables and tiered log segments for Log Tables on a JuiceFS volume. + +## Install JuiceFS Plugin on Server Nodes + +JuiceFS support is not included in the default Fluss distribution. To enable JuiceFS support on the server side (CoordinatorServer and every TabletServer), you need to manually install the filesystem plugin into Fluss. + +1. **Prepare the plugin JAR**: + + - Download the `fluss-fs-juicefs-$FLUSS_VERSION$.jar` from the [Maven Repository](https://repo1.maven.org/maven2/org/apache/fluss/fluss-fs-juicefs/$FLUSS_VERSION$/fluss-fs-juicefs-$FLUSS_VERSION$.jar). + +2. **Place the plugin**: Place the plugin JAR file in the `${FLUSS_HOME}/plugins/juicefs/` directory: + ```bash + mkdir -p ${FLUSS_HOME}/plugins/juicefs/ + cp fluss-fs-juicefs-$FLUSS_VERSION$.jar ${FLUSS_HOME}/plugins/juicefs/ + ``` + +3. Restart Fluss if the cluster is already running to ensure the new plugin is loaded. + +:::note +The `${FLUSS_HOME}/plugins/` directory is only consulted by the Fluss server's plugin manager. Fluss clients (Flink connector, Spark connector, standalone applications, ...) do not load plugins from this directory — see [Client-side Setup](#client-side-setup) for how to enable JuiceFS on the client. +::: + +## Prerequisite: Create a JuiceFS Volume + +Before Fluss can use JuiceFS as remote storage, a JuiceFS volume must be created in advance using the JuiceFS CLI (`juicefs format ...`). The volume creation step configures: + +- The **metadata engine** (e.g. Redis, TiKV, MySQL, PostgreSQL). +- The backing **object storage** and its credentials (baked into the volume so individual clients do not need to re-configure them). +- The **volume name**, which becomes the authority in the `jfs:///...` URI. + +Refer to the [JuiceFS Getting Started guide](https://juicefs.com/docs/community/getting-started/installation) for volume creation instructions. Make sure the metadata engine and the object storage are reachable from every Fluss node (CoordinatorServer, TabletServer, and clients). + +## Configurations setup + +To enable JuiceFS as remote storage, add the following required configurations to Fluss' `server.yaml`: + +```yaml +# The dir that used to be as the remote storage of Fluss +remote.data.dir: jfs:///path/to/remote/storage +# JuiceFS metadata engine address of the pre-created volume, +# e.g. redis://:/, tikv://..., mysql://..., postgres://... +juicefs.meta: +``` + +Only Fluss configuration keys with the prefix `fs.jfs.` or `juicefs.` are forwarded from `server.yaml` to the underlying Hadoop `Configuration` consumed by the JuiceFS SDK. Any other keys are ignored by the JuiceFS plugin. + +The plugin also auto-injects the following defaults when they are not set by the user, so you normally do **not** need to configure them explicitly: + +```yaml +fs.jfs.impl: io.juicefs.JuiceFileSystem +fs.jfs.impl.disable.cache: false +``` + +A typical configuration that additionally enables local disk cache and an access log looks like: + +```yaml +remote.data.dir: jfs:///path/to/remote/storage +juicefs.meta: redis://:/ + +# Local cache directories (create them in advance with mode 0777). +# Multiple paths can be separated by ":", wildcards such as "*" are supported. +juicefs.cache-dir: /data*/jfscache +# Total local cache capacity across all cache dirs, in MiB. +juicefs.cache-size: 1024 +# Path of the access log file (auto-rotated, latest 7 files retained). +juicefs.access-log: /tmp/juicefs.access.log +``` + +## Authentication + +Unlike the OSS / S3 / COS plugins, Fluss does **not** perform any STS or delegation-token exchange for JuiceFS. The JuiceFS client authenticates locally on each node: + +- Against the **metadata engine** using the URL configured via `juicefs.meta`. +- Against the backing **object storage** using the credentials that were provided when the volume was formatted (or via optional `juicefs.access-key` / `juicefs.secret-key` overrides, if applicable). + +The Fluss server returns an empty placeholder security token to clients when they request access to the remote storage. This means every process that needs to read remote data (CoordinatorServer, TabletServer, and every client) must: + +1. Have direct network access to the JuiceFS metadata engine and object storage. +2. Have the JuiceFS plugin available in its own runtime. Servers load the plugin from `${FLUSS_HOME}/plugins/juicefs/` — see [Install JuiceFS Plugin on Server Nodes](#install-juicefs-plugin-on-server-nodes). Clients use a different mechanism — see [Client-side Setup](#client-side-setup). + +## Client-side Setup + +Any application that reads Fluss remote data through the Fluss client will fetch tiered log segments and Primary-Key Table snapshots from JuiceFS via a `FileSystem` instance created inside the client process. Enabling JuiceFS on the client therefore requires two independent steps: making the plugin visible to the client's classloader, and forwarding the JuiceFS configuration through `client.fs.*` keys. + +### 1. Add the JuiceFS plugin JAR to the application classpath + +Client process needs to bundle `fluss-fs-juicefs-$FLUSS_VERSION$.jar` on its own application classpath. The plugin JAR already shades `io.juicefs.JuiceFileSystem` and the required JuiceFS Hadoop SDK classes, so no additional JuiceFS dependency is needed. + +Connector-specific placement: + +- **Flink connector**: add `fluss-fs-juicefs-$FLUSS_VERSION$.jar` into Flink's `lib/` directory on every JobManager and TaskManager. +- **Spark connector**: add `--jars /path/to/fluss-fs-juicefs-$FLUSS_VERSION$.jar` to `spark-submit` / `spark-sql`, or install it into Spark's `jars/` directory on every driver and executor node. + +### 2. Configure JuiceFS via `client.fs.*` keys + +`FlussConnection` only forwards Fluss configuration entries whose keys start with `client.fs.`, and the `client.fs.` prefix is stripped before the map is handed to `FileSystem.initialize`. The JuiceFS plugin in turn only picks up keys starting with `fs.jfs.` or `juicefs.`. In practice this means every JuiceFS knob you want to expose on the client must be written as: + +``` +client.fs. = +``` + +For example, `client.fs.juicefs.access-key=xxxx` is forwarded to the JuiceFS plugin as `juicefs.access-key=xxxx`. + +Raw `juicefs.*` or `fs.jfs.*` keys that are **not** prefixed with `client.fs.` are silently dropped by `FlussConnection` and never reach the JuiceFS plugin. This is the most common client-side misconfiguration. + +Because Fluss does not perform any credential delegation for JuiceFS (see [Authentication](#authentication)), each client host must also have valid credentials to the object storage backing the volume. For example, `client.fs.juicefs.access-key` / `client.fs.juicefs.secret-key`. + +### Connector examples + +**Flink SQL / Table API.** Connector options declared in `WITH (...)` are propagated into the underlying client `Configuration`, so JuiceFS options can be set per-table: + +```sql +CREATE TABLE fluss_table ( + ... +) WITH ( + 'connector' = 'fluss', + 'bootstrap.servers' = ':', + 'client.fs.juicefs.meta' = 'redis://:/', + 'client.fs.juicefs.cache-dir' = '/data*/jfscache', + 'client.fs.juicefs.cache-size' = '1024' +); +``` + +Alternatively, set them globally in `flink-conf.yaml` / at cluster level so they apply to every Fluss table. + +**Spark.** Pass them as Fluss catalog options, either on the command line or in `spark-defaults.conf`: + +```bash +spark-sql \ + --jars /path/to/fluss-fs-juicefs-$FLUSS_VERSION$.jar \ + --conf spark.sql.catalog.fluss_catalog=org.apache.fluss.spark.catalog.FlussCatalog \ + --conf spark.sql.catalog.fluss_catalog.bootstrap.servers=: \ + --conf spark.sql.catalog.fluss_catalog.client.fs.juicefs.meta=redis://:/ +``` + +## Advanced Configurations + +Apart from the configurations above, any other JuiceFS Hadoop SDK configuration key can be defined in Fluss' `server.yaml` as long as it starts with `fs.jfs.` or `juicefs.` — the plugin will forward it to the underlying Hadoop configuration. Refer to the [JuiceFS Hadoop Java SDK documentation](https://juicefs.com/docs/community/hadoop_java_sdk) for the complete parameter reference. Commonly used tuning knobs include: + +- **Cache**: `juicefs.cache-dir`, `juicefs.cache-size`, `juicefs.cache-full-block`, `juicefs.free-space`, `juicefs.attr-cache`, `juicefs.entry-cache`, `juicefs.dir-entry-cache` +- **I/O**: `juicefs.max-uploads`, `juicefs.max-downloads`, `juicefs.memory-size`, `juicefs.prefetch`, `juicefs.io-retries`, `juicefs.upload-limit`, `juicefs.download-limit` +- **Miscellaneous**: `juicefs.access-log`, `juicefs.debug`, `juicefs.block.size`, `juicefs.bucket` + +These configurations are advanced options that are usually used for performance tuning. diff --git a/website/docs/maintenance/tiered-storage/filesystems/overview.md b/website/docs/maintenance/tiered-storage/filesystems/overview.md index c02abbda67d..ddbe2ff54eb 100644 --- a/website/docs/maintenance/tiered-storage/filesystems/overview.md +++ b/website/docs/maintenance/tiered-storage/filesystems/overview.md @@ -7,7 +7,7 @@ sidebar_position: 1 # File Systems Fluss uses file systems as remote storage to store snapshots for Primary-Key Table and store tiered log segments for Log Table. These -are some of the file systems that Fluss supports currently, including *local*, *hadoop*, *Aliyun OSS*, *Tencent Cloud COS*. +are some of the file systems that Fluss supports currently, including *local*, *hadoop*, *Aliyun OSS*, *Tencent Cloud COS*, *JuiceFS*. The file system used for a particular file is determined by its URI scheme. For example, `file:///home/user/text.txt` refers to a file in the local file system, while `hdfs://namenode:50010/data/user/text.txt` is a file in a specific HDFS cluster. @@ -42,4 +42,6 @@ The Fluss project supports the following file systems: - **[Tencent Cloud COS](cos.md)** is supported by `fluss-fs-cos` and registered under the `cosn://` URI scheme. Please make sure to [manually install the COS plugin](cos.md#install-cos-plugin-manually). +- **[JuiceFS](juicefs.md)** is supported by `fluss-fs-juicefs` and registered under the `jfs://` URI scheme. Please make sure to [manually install the JuiceFS plugin](juicefs.md#install-juicefs-plugin-manually). + The implementation is based on [Hadoop Project](https://hadoop.apache.org/) but is self-contained with no dependency footprint. \ No newline at end of file