Repository: juicedata/juicefs
Pull request #5536 (2025-01-09 12:38:50)

Gateway: add object metadata
Enable object metadata for s3 gateway
#5367

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/juicedata/juicefs?pullRequest=5536) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/juicedata/juicefs?pullRequest=5536) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/juicedata/juicefs?pullRequest=5536) it.</sub>

Patch:
diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml
index 6685e4e3..7bfc0c76 100644
--- a/.github/actions/build/action.yml
+++ b/.github/actions/build/action.yml
@@ -5,11 +5,9 @@ inputs:
     description: 'build target: juicefs, juicefs.fdb etc'
     required: true
     default: 'juicefs'
-  useBeta:
-    description: 'use beta version of juicefs to skip build'
-    required: true
-    type: boolean
-    default: false
+  beta:
+    description: 'beta version for the following test'
+    required: false
 runs:
   using: "composite"
   steps:
@@ -43,29 +41,15 @@ runs:
     - name: Build linux target
       shell: bash
       run: |
-        if ${{ inputs.useBeta }}; then
-          echo "try use beta version of juicefs"
-          if wget -q https://juicefs-pub.oss-cn-hangzhou.aliyuncs.com/${{inputs.target}}; then
-            [ "${{inputs.target}}" != "juicefs" ] && mv ${{inputs.target}} juicefs
-            sudo chmod +x juicefs
-            version=$(./juicefs version)
-            date1=$( echo $version | grep -oP '\d{4}-\d{2}-\d{2}')
-            date2=$(date +%Y-%m-%d)
-            if [ "$date1" == "$date2" ]; then
-              echo beta version is up to date, $version, use it to skip build
-              exit 0
-            else
-              echo beta version is out of date, $version, remove it and build
-              rm -f juicefs
-            fi
-          fi
-        fi
-        echo "start to build ${{inputs.target}}"
-        make ${{inputs.target}}.cover
-        if ${{ inputs.useBeta }}; then
-          echo "upload beta version"
-          curl --upload-file ${{inputs.target}} https://juicefs-pub.oss-cn-hangzhou.aliyuncs.com/
-          echo "upload beta version succeed"
-        fi
-        [ "${{inputs.target}}" != "juicefs" ] &&  mv ${{inputs.target}} juicefs
-        echo "build ${{inputs.target}} succeed"
\ No newline at end of file
+        if [[ -n "${{ inputs.beta }}" ]]; then
+          echo "use beta version of juicefs: ${{inputs.beta}}"
+          wget -q https://juicefs-com-static.oss-cn-shanghai.aliyuncs.com/juicefs_beta/${{inputs.beta}} -O juicefs
+          chmod +x juicefs
+          ./juicefs version
+        else
+          echo "start to build ${{inputs.target}}"
+          make ${{inputs.target}}.cover
+          [ "${{inputs.target}}" != "juicefs" ] &&  mv ${{inputs.target}} juicefs
+          ./juicefs version
+          echo "build ${{inputs.target}} succeed"
+        fi
\ No newline at end of file
diff --git a/.github/scripts/cache.sh b/.github/scripts/cache.sh
new file mode 100755
index 00000000..676bf755
--- /dev/null
+++ b/.github/scripts/cache.sh
@@ -0,0 +1,514 @@
+#!/bin/bash -e
+dpkg -s redis-server || .github/scripts/apt_install.sh  redis-tools redis-server
+dpkg -s fio || .github/scripts/apt_install.sh fio
+source .github/scripts/common/common.sh
+source .github/scripts/start_meta_engine.sh
+[[ -z "$META" ]] && META=sqlite3
+start_meta_engine $META minio
+META_URL=$(get_meta_url $META)
+
+test_warmup_in_background(){
+    prepare_test
+    ./juicefs format $META_URL myjfs --trash-days 0
+    ./juicefs mount $META_URL /tmp/jfs -d
+    dd if=/dev/zero of=/tmp/jfs/test bs=1M count=1024
+    ./juicefs warmup /tmp/jfs/test --evict
+    ./juicefs warmup /tmp/jfs/test --background
+    wait_warmup_finish /tmp/jfs/test 100
+    ./juicefs warmup /tmp/jfs/test --background --evict 
+    wait_warmup_finish /tmp/jfs/test 0
+}
+
+test_batch_warmup(){
+    prepare_test
+    ./juicefs format $META_URL myjfs --trash-days 0
+    ./juicefs mount $META_URL /tmp/jfs -d
+    rm -f file.list
+    file_count=11000
+    time seq 1 $file_count | xargs -P 8 -I {} sh -c 'echo {} > /tmp/jfs/test_{}; echo /tmp/jfs/test_{} >> file.list'
+    # time for i in $(seq 1 $file_count); do echo $i > /tmp/jfs/test_$i; echo /tmp/jfs/test_$i >> file.list; done
+    ./juicefs warmup -f file.list 2>&1 | tee warmup.log
+    files=$(get_cache_file_count)
+    [[ $files -ne $file_count ]] && echo "warmup failed, expect $file_count files, actual $files" && exit 1 || true
+    ./juicefs warmup -f file.list --check 2>&1 | tee warmup.log
+    files=$(get_cache_file_count)
+    [[ $files -ne $file_count ]] && echo "warmup failed, expect $file_count files, actual $files" && exit 1 || true
+    grep "(100.0%)" warmup.log || (echo "warmup failed, expect 100.0% warmup" && exit 1)
+    ./juicefs warmup -f file.list --evict 2>&1 | tee warmup.log 
+    files=$(get_cache_file_count)
+    [[ $files -ne $file_count ]] && echo "warmup evict failed, expect $file_count files, actual $files" && exit 1 || true
+    ./juicefs warmup -f file.list --check 2>&1 | tee warmup.log
+    files=$(get_cache_file_count)
+    [[ $files -ne $file_count ]] && echo "warmup evict failed, expect $file_count files, actual $files" && exit 1 || true
+    grep "(0.0%)" warmup.log || (echo "warmup failed, expect 0.0% warmup" && exit 1)
+
+    ./juicefs warmup /tmp/jfs/test* 2>&1 | tee warmup.log
+    files=$(get_cache_file_count)
+    [[ $files -ne $file_count ]] && echo "warmup failed, expect $file_count files, actual $files" && exit 1 || true
+}
+
+test_kernel_writeback_cache(){
+    prepare_test
+    ./juicefs format $META_URL myjfs --trash-days 0
+    ./juicefs mount $META_URL /tmp/jfs -d -o writeback_cache
+    mkdir /tmp/jfs/fio
+    runtime=15
+    cat /tmp/jfs/.stats | grep fuse | grep 'juicefs_fuse_written_size_bytes_sum\|juicefs_fuse_ops_total_write'
+    fio --name=seq_write_test --rw=write --bs=10 --size=4M --numjobs=8 --nrfiles=1 --runtime=$runtime --time_based --group_reporting --directory=/tmp/jfs/fio | tee fio.log
+    cat /tmp/jfs/.stats | grep fuse | grep 'juicefs_fuse_written_size_bytes_sum\|juicefs_fuse_ops_total_write'
+    bytes=$(cat /tmp/jfs/.stats | grep juicefs_fuse_written_size_bytes_sum | awk '{print $2}')
+    ops=$(cat /tmp/jfs/.stats | grep juicefs_fuse_ops_total_write | awk '{print $2}')
+    [[ $((bytes/ops)) -lt 10240 ]] && echo "writeback_cache may not enabled" && exit 1 || true
+}
+
+test_cache_items(){
+    prepare_test
+    ./juicefs format $META_URL myjfs
+    cache_items=500
+    ./juicefs mount $META_URL /tmp/jfs -d --cache-items $cache_items
+    seq 1 $((cache_items*2)) | xargs -P 8 -I {} sh -c 'echo {} > /tmp/jfs/test_{};'
+    ./juicefs warmup /tmp/jfs/
+    ./juicefs warmup /tmp/jfs/ --check 2>&1 | tee warmup.log
+    ratio=$(get_warmup_ratio)
+    [[ $ratio -lt 55 ]] || (echo "ratio should less than 55%" && exit 1)
+}
+
+test_evict_on_writeback(){
+    prepare_test
+    ./juicefs format $META_URL myjfs --compress zstd
+    ./juicefs mount $META_URL /tmp/jfs -d --writeback --upload-delay 3s
+    dd if=/dev/urandom of=/tmp/test bs=1M count=200
+    cp /tmp/test /tmp/jfs/test
+    sleep 3
+    stageBlocks=$(grep "juicefs_staging_blocks" /tmp/jfs/.stats | awk '{print $2}')
+    [[ $stageBlocks -eq 0 ]] && echo "stage blocks should not be 0" && exit 1 || true
+    ./juicefs warmup /tmp/jfs/test --evict
+    wait_stage_uploaded
+    compare_md5sum /tmp/test /tmp/jfs/test
+}
+
+test_remount_on_writeback(){
+    prepare_test
+    ./juicefs format $META_URL myjfs --compress lz4
+    ./juicefs mount $META_URL /tmp/jfs -d --writeback --upload-delay 3s
+    dd if=/dev/urandom of=/tmp/test bs=1M count=200
+    cp /tmp/test /tmp/jfs/test
+    umount_jfs /tmp/jfs $META_URL
+    ./juicefs mount $META_URL /tmp/jfs -d --writeback
+    sleep 3
+    stage_size=$(du -shm $(get_rawstaging_dir) | awk '{print $1}')
+    [[ $stage_size -gt 2 ]] && echo "stage size should not great than 2M" && exit 1 || true
+    ./juicefs warmup /tmp/jfs/test --evict
+    compare_md5sum /tmp/test /tmp/jfs/test
+}
+test_memory_cache_none(){
+    do_test_memory_cache none
+}
+
+test_memory_cache_2_random(){
+    do_test_memory_cache 2-random
+}
+
+do_test_memory_cache(){
+    cache_eviction=$1
+    prepare_test
+    ./juicefs format $META_URL myjfs --compress lz4
+    ./juicefs mount $META_URL /tmp/jfs -d --cache-dir memory --cache-size 100M --cache-eviction $cache_eviction
+    dd if=/dev/zero of=/tmp/jfs/test bs=1M count=200
+    ./juicefs warmup /tmp/jfs/test
+    ./juicefs warmup /tmp/jfs/test --check 2>&1 | tee warmup.log
+    ratio=$(get_warmup_ratio)
+    if [[ $cache_eviction == "2-random" ]]; then
+        [[ "$ratio" -gt 40 && "$ratio" -lt 60   ]] || (echo "ratio($ratio) should between 40% and 60%" && exit 1)
+    elif [[ $cache_eviction == "none" ]]; then
+        [[ "$ratio" -gt 40 && "$ratio" -lt 60   ]] || (echo "ratio($ratio) should between 40% and 60%" && exit 1)
+    fi
+    ./juicefs warmup /tmp/jfs/test --evict
+    ./juicefs warmup /tmp/jfs/test --check 2>&1 | tee warmup.log
+    ratio=$(get_warmup_ratio)
+    [[ "$ratio" = 0 ]] || (echo "ratio($ratio) should less than 0" && exit 1)
+}
+
+test_cache_expired(){
+    do_test_cache_expired /var/jfsCache/myjfs
+}
+
+test_cache_expired_memory(){
+    do_test_cache_expired memory
+}
+
+do_test_cache_expired(){
+    cache_dir=$1
+    prepare_test
+    ./juicefs format $META_URL myjfs
+    ./juicefs mount $META_URL /tmp/jfs -d --cache-dir $cache_dir --cache-expire 3s
+    dd if=/dev/zero of=/tmp/jfs/test bs=1M count=200
+    for i in $(seq 1 1100); do
+        dd if=/dev/zero of=/tmp/jfs/test$i bs=32k count=1 status=none
+    done
+    ./juicefs warmup /tmp/jfs/ 2>&1 | tee warmup.log
+    sleep 15
+    ./juicefs warmup /tmp/jfs/ --check 2>&1 | tee warmup.log
+    grep "(0.0%)" warmup.log || (echo "cache should expired" && exit 1)
+}
+
+test_cache_large_write(){
+    prepare_test
+    ./juicefs format $META_URL myjfs
+    ./juicefs mount $META_URL /tmp/jfs -d
+    dd if=/dev/zero of=/tmp/jfs/test bs=1M count=200
+    ./juicefs warmup /tmp/jfs/test --check 2>&1 | tee warmup.log
+    ratio=$(get_warmup_ratio)
+    [[ "$ratio" = 0 ]] || (echo "ratio($ratio) should less than 0" && exit 1)
+    ./juicefs mount $META_URL /tmp/jfs -d --cache-large-write 
+    dd if=/dev/zero of=/tmp/jfs/test1 bs=1M count=200
+    ./juicefs warmup /tmp/jfs/test1 --check 2>&1 | tee warmup.log
+    # TODO: should check the ratio
+    check_warmup_log 90
+}
+
+test_cache_mode(){
+    prepare_test
+    ./juicefs format $META_URL myjfs
+    cache_mode=$(printf "%03o" $((RANDOM % 512)))
+    echo "cache mode is $cache_mode"
+    ./juicefs mount $META_URL /tmp/jfs -d --cache-mode $cache_mode --writeback --upload-delay 3s
+    dd if=/dev/zero of=/tmp/jfs/test bs=1M count=32
+    ./juicefs warmup /tmp/jfs/test
+    find $(get_raw_dir) -type f ! -perm $cache_mode -exec echo "perm of {} is incorrect" \; -exec false {} +
+    find $(get_rawstaging_dir) -type f ! -perm $cache_mode -exec echo "perm of {} is incorrect" \; -exec false {} +
+    sleep 5s 
+    find $(get_raw_dir) -type f ! -perm $cache_mode -exec echo "perm of {} is incorrect" \; -exec false {} +
+    find $(get_rawstaging_dir) -type f ! -perm $cache_mode -exec echo "perm of {} is incorrect" \; -exec false {} +
+}
+
+test_cache_compressed(){
+    prepare_test
+    ./juicefs format $META_URL myjfs --storage minio --bucket http://localhost:9000/test \
+        --access-key minioadmin --secret-key minioadmin --compress lz4 --hash-prefix
+    ./juicefs mount $META_URL /tmp/jfs -d 
+    dd if=/dev/urandom of=/tmp/test bs=1M count=200
+    cp /tmp/test /tmp/jfs/test
+    ./juicefs warmup /tmp/jfs/test --evict
+    ./juicefs warmup /tmp/jfs/test
+    docker stop minio
+    compare_md5sum /tmp/test /tmp/jfs/test
+    docker start minio
+}
+
+test_cache_checksum_none(){
+    do_test_cache_checksum none
+}
+
+test_cache_checksum_full(){
+    do_test_cache_checksum full
+}
+
+test_cache_checksum_shrink(){
+    do_test_cache_checksum shrink
+}
+
+test_cache_checksum_extend(){
+    do_test_cache_checksum extend
+}
+
+do_test_cache_checksum(){
+    checksum_level=$1
+    prepare_test
+    ./juicefs format $META_URL myjfs --compress lz4
+    ./juicefs mount $META_URL /tmp/jfs -d --verify-cache-checksum $checksum_level
+    mkdir -p /tmp/jfs/rand-rw
+    fio --name=seq_rw --rw=readwrite --bsrange=1k-4k --size=80M --numjobs=4 --runtime=5 --time_based --group_reporting --filename=/tmp/jfs/req-rw
+    fio --name=rand_rw   --rw=randrw --bsrange=1k-4k --size=80M --numjobs=4 --runtime=5 --time_based --group_reporting --directory=/tmp/jfs/rand-rw --nrfiles=1000 --filesize=4k
+}
+
+test_disk_full_2_random(){
+    do_test_disk_full 2-random
+}
+
+test_disk_full_none(){
+    do_test_disk_full none
+}
+
+do_test_disk_full(){
+    cache_eviction=$1
+    prepare_test
+    mount_jfsCache1 1G
+    ./juicefs format $META_URL myjfs 
+    ./juicefs mount $META_URL /tmp/jfs -d --cache-dir /var/jfsCache1 --cache-eviction $cache_eviction --free-space-ratio 0.2
+    dd if=/dev/zero of=/tmp/test bs=1M count=1200
+    cp /tmp/test /tmp/jfs/test
+    ./juicefs warmup /tmp/jfs/test
+    sleep 3 # wait to free space
+    df -h /var/jfsCache1
+    ./juicefs warmup /tmp/jfs/test --check 2>&1 | tee warmup.log
+    used_percent=$(df /var/jfsCache1 | tail -1  | awk '{print $5}' | tr -d %)
+    echo "used percent is $used_percent"
+    if [[ $cache_eviction == "2-random" ]]; then 
+        [[ $used_percent -gt 80 ]] && echo "used percent($used_percent) should not more than 80%" && exit 1 || true
+    elif [[ $cache_eviction == "none" ]]; then
+        # cache will not evict even reach the free-space-ratio.
+        [[ $used_percent -lt 80 ]] && echo "used percent($used_percent) should not less than 80%" && exit 1 || true
+    fi
+}
+
+test_inode_full(){
+    prepare_test
+    mount_jfsCache1 100G 1000
+    ./juicefs format $META_URL myjfs
+    ./juicefs mount $META_URL /tmp/jfs -d --cache-dir /var/jfsCache1 --free-space-ratio 0.2
+    seq 1 1000 | xargs -P 8 -I {} sh -c 'echo {} > /tmp/jfs/test_{};'
+    ./juicefs warmup /tmp/jfs/
+    ./juicefs warmup /tmp/jfs/ --check 2>&1 | tee warmup.log
+    sleep 3
+    used_percent=$(df -i /var/jfsCache1 | tail -1  | awk '{print $5}' | tr -d %)
+    [[ $used_percent -gt 85 ]] && echo "used percent($used_percent) should less than 85%" && exit 1 || true
+}
+
+test_disk_full_with_writeback(){
+    prepare_test
+    mount_jfsCache1 1G
+    ./juicefs format $META_URL myjfs --compress zstd
+    ./juicefs mount $META_URL /tmp/jfs -d --cache-dir /var/jfsCache1 --writeback --free-space-ratio 0.2 --upload-delay 5s
+    dd if=/dev/urandom of=/tmp/test bs=1M count=1400
+    cp /tmp/test /tmp/jfs/test
+    wait_stage_uploaded
+    sleep 3
+    used_percent=$(df /var/jfsCache1 | tail -1  | awk '{print $5}' | tr -d %)
+    [[ $used_percent -gt 80 ]] && echo "used percent($used_percent) should less than 80%" && exit 1 || true
+    echo 3 > /proc/sys/vm/drop_caches
+    ./juicefs warmup /tmp/jfs/test --evict
+    compare_md5sum /tmp/test /tmp/jfs/test
+}
+
+test_disk_failover()
+{
+    prepare_test
+    mount_jfsCache1
+    rm -rf /var/log/juicefs.log
+    rm -rf /var/jfsCache2 /var/jfsCache3
+    ./juicefs format $META_URL myjfs --trash-days 0 --storage minio --bucket http://localhost:9000/test --access-key minioadmin --secret-key minioadmin
+    JFS_MAX_DURATION_TO_DOWN=10s JFS_MAX_IO_DURATION=3s ./juicefs mount $META_URL /tmp/jfs -d \
+        --cache-dir=/var/jfsCache1:/var/jfsCache2:/var/jfsCache3 --io-retries 1 
+    dd if=/dev/urandom of=/tmp/test bs=1M count=1024
+    cp /tmp/test /tmp/jfs/test
+    /etc/init.d/redis-server stop
+    ./juicefs warmup /tmp/jfs/test
+    ./juicefs warmup --check /tmp/jfs 2>&1 | tee warmup.log
+    check_warmup_log  50
+    wait_disk_down 60
+    ./juicefs warmup /tmp/jfs/test
+    ./juicefs warmup --check /tmp/jfs 2>&1 | tee warmup.log
+    check_warmup_log 98
+    check_cache_distribute 1024 /var/jfsCache2 /var/jfsCache3
+    echo stop minio && docker stop minio
+    compare_md5sum /tmp/test /tmp/jfs/test
+    docker start minio && sleep 3
+}
+
+test_disk_failure_on_writeback()
+{
+    prepare_test
+    mount_jfsCache1
+    rm -rf /var/log/juicefs.log
+    rm -rf /var/jfsCache2 /var/jfsCache3
+    mkdir -p /var/jfsCache2 /var/jfsCache3
+    ./juicefs format $META_URL myjfs --trash-days 0 --storage minio --bucket http://localhost:9000/test --access-key minioadmin --secret-key minioadmin
+    JFS_MAX_DURATION_TO_DOWN=5s JFS_MAX_IO_DURATION=3s ./juicefs mount $META_URL /tmp/jfs -d \
+        --cache-dir=/var/jfsCache? --io-retries 1 --writeback -v
+    dd if=/dev/urandom of=/tmp/test bs=1M count=1024
+    cp /tmp/test /tmp/jfs/test
+    dd if=/dev/urandom of=/tmp/jfs/test2 bs=1M count=10
+    /etc/init.d/redis-server stop
+    ./juicefs warmup /tmp/jfs/test2 &
+    sleep 15
+    grep -q "state change from unstable to down" /var/log/juicefs.log && echo "disk should not down" && exit 1 || true
+    /etc/init.d/redis-server start
+    ./juicefs warmup /tmp/jfs/test
+    ./juicefs warmup /tmp/jfs/test --check 2>&1 | tee warmup.log
+    # TODO: the ratio should be 100%
+    check_warmup_log 60
+    check_cache_distribute 1024 /var/jfsCache1 /var/jfsCache2 /var/jfsCache3
+    compare_md5sum /tmp/test /tmp/jfs/test
+}
+
+prepare_test()
+{
+    df -h /
+    umount_jfs /tmp/jfs $META_URL
+    python3 .github/scripts/flush_meta.py $META_URL
+    rm -rf /var/jfs/myjfs || true
+    rm -rf /var/jfsCache/myjfs || true
+    [[ ! -f /usr/local/bin/mc ]] && wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc -O /usr/local/bin/mc && chmod +x /usr/local/bin/mc
+    mc alias set myminio http://localhost:9000 minioadmin minioadmin
+    mc rm --force --recursive myminio/test || true
+}
+
+wait_warmup_finish(){
+    path=$1
+    expected_ratio=$2
+    timeout=30
+    for i in $(seq 1 $timeout); do
+        ./juicefs warmup $path --check 2>&1 |tee warmup.log
+        ratio=$(get_warmup_ratio)
+        if [[ "$ratio" == "$expected_ratio" ]]; then
+            echo "warmup finished after $i seconds, ratio is $ratio, expected ratio is $expected_ratio"
+            break
+        else
+            echo "wait warmup finish $i"
+            sleep 1
+        fi
+        if [[ $i -eq $timeout ]]; then
+            echo "wait warmup finish timeout after $timeout seconds" && exit 1
+        fi
+    done
+}
+
+wait_stage_uploaded()
+{
+    echo "wait stage upload"
+    for i in {1..30}; do
+        stageBlocks=$(grep "juicefs_staging_blocks" /tmp/jfs/.stats | awk '{print $2}')
+        if [[ "$stageBlocks" -eq 0 ]]; then
+            echo "stageBlocks is now 0"
+            break
+        fi
+        echo "wait stage upload $i" && sleep 1
+    done
+    if [[ "$stageBlocks" -ne 0 ]]; then
+        echo "stage blocks have not uploaded: $stageBlocks" && exit 1
+    fi
+}
+
+mount_jfsCache1(){
+    capacity=$1
+    [[ -z $capacity ]] && capacity=100G
+    inodes=$2
+    [[ -z $inodes ]] && inodes=10000000
+    /etc/init.d/redis-server start
+    timeout 30s bash -c 'until nc -zv localhost 6379; do sleep 1; done'
+    umount -l /var/jfsCache1 || true
+    rm -rf /var/jfsCache1
+    redis-cli flushall
+    rm -rf /var/jfs/test
+    ./juicefs format "redis://localhost/1?read-timeout=3&write-timeout=1&max-retry-backoff=3" test --trash-days 0 --capacity $capacity --inodes $inodes
+    ./juicefs mount redis://localhost/1 /var/jfsCache1 -d --log /tmp/juicefs.log
+    # trap "echo umount /var/jfsCache1 && umount -l /var/jfsCache1" EXIT
+}
+
+get_cache_dir(){
+    grep CacheDir /tmp/jfs/.config | awk -F'"' '{print $4}'
+}
+
+get_raw_dir(){
+    echo $(get_cache_dir)/raw/
+}
+
+get_rawstaging_dir(){
+    echo $(get_cache_dir)/rawstaging/
+}
+
+check_evict_log(){
+    ratio=$(get_warmup_ratio)
+    if [[ "$ratio" -gt 0 ]]; then
+        echo "cache ratio($ratio) should be 0 after evict"
+        exit 1
+    fi
+}
+
+check_warmup_log(){
+    expected_ratio=$1
+    ratio=$(get_warmup_ratio)
+    if [[ "$ratio" -lt "$expected_ratio" ]]; then
+        echo "cache ratio($ratio) should be more than expected_ratio($expected_ratio) after warmup"
+        exit 1
+    fi
+}
+
+get_cache_file_count(){
+    sed -n 's/.* \([0-9]\+\) files.*/\1/p' warmup.log
+}
+
+get_cache_file_size(){
+    sed -n 's/.* \([0-9]*\) MiB of.*/\1/p' warmup.log
+}
+
+get_warmup_ratio(){
+    sed -n 's/.*(\([0-9]*\.[0-9]*%\)).*/\1/p' warmup.log | sed 's/%//' | awk '{print int($1)}'
+}
+
+
+check_cache_distribute() {
+    max_total_size=$(echo "$1 * 1024" | bc | awk '{printf "%.0f", $1}')
+    echo check_cache_distribute, max_total_size is $max_total_size
+    shift
+    total_weight=0
+    declare -A weights
+    declare -A sizes
+    # Parse directory names and weights
+    for arg in "$@"; do
+        dir=$(echo "$arg" | awk -F: '{print $1}')
+        weight=$(echo "$arg" | awk -F: '{print $2}')
+        if [[ -z $weight ]]; then
+            weight=1
+        fi
+        weights["$dir"]=$weight
+        total_weight=$((total_weight + weight))
+    done
+    
+    # Calculate total size and sizes of each directory
+    for dir in "${!weights[@]}"; do
+        echo dir is $dir
+        du -sh "$dir" || true
+        size=$(du -s "$dir" | awk '{print $1}')
+        echo size is $size
+        sizes["$dir"]=$size
+    done
+    
+    # Check if total size exceeds max limit
+    total_size=0
+    for dir in "${!sizes[@]}"; do
+        size=${sizes["$dir"]}
+        total_size=$((total_size + size))
+    done
+    echo "total size is $total_size, max_total_size is $max_total_size"
+    if [[ $total_size -gt $((max_total_size + max_total_size/10)) ]]; then
+        echo "Total size of directories exceeds max limit"
+        return 1
+    fi
+    
+    # Check if each directory is evenly distributed based on its weight
+    for dir in "${!sizes[@]}"; do
+        size=${sizes["$dir"]}
+        weight=${weights["$dir"]}
+        avg_size=$((total_size * weight / total_weight))
+        min_size=$((avg_size * 5 / 10))
+        max_size=$((avg_size * 20 / 10))
+        
+        if [[ $size -lt $min_size || $size -gt $max_size ]]; then
+            echo "$dir is not evenly distributed, size: $size, weight: $weight, ave_size: $avg_size, min_size: $min_size, max_size: $max_size"
+            exit 1
+        else
+            echo "$dir is evenly distributed"
+        fi
+    done
+}
+
+wait_disk_down()
+{
+    timeout=$1
+    for i in $(seq 1 $timeout); do
+        if grep -q "state change from unstable to down" /var/log/juicefs.log; then
+            echo "state changed from unstable to down after $i seconds"
+            return
+        else
+            echo "\rWait for state change to down, $i"
+            sleep 1
+            count=$((count+1))
+        fi
+    done
+    echo "Wait for state change to down timeout after $timeout seconds" && exit 1
+}   
+
+source .github/scripts/common/run_test.sh && run_test $@
+
diff --git a/.github/scripts/check_juicefs_log.sh b/.github/scripts/check_juicefs_log.sh
new file mode 100755
index 00000000..95a923b3
--- /dev/null
+++ b/.github/scripts/check_juicefs_log.sh
@@ -0,0 +1,9 @@
+#!/bin/bash -e
+for log_file in /var/log/juicefs.log $HOME/.juicefs/juicefs.log; do
+    if [ -f $log_file ]; then
+        break
+    fi
+done
+echo "tail -1000 $log_file"
+tail -1000 $log_file
+grep -i "<FATAL>\|panic" $log_file && exit 1 || true
\ No newline at end of file
diff --git a/.github/scripts/command/cache.sh b/.github/scripts/command/cache.sh
deleted file mode 100755
index 70ad44a1..00000000
--- a/.github/scripts/command/cache.sh
+++ /dev/null
@@ -1,147 +0,0 @@
-#!/bin/bash -e
-dpkg -s redis-server || .github/scripts/apt_install.sh  redis-tools redis-server
-source .github/scripts/common/common.sh
-source .github/scripts/start_meta_engine.sh
-[[ -z "$META" ]] && META=sqlite3
-start_meta_engine $META minio
-META_URL=$(get_meta_url $META)
-TEST_FILE_SIZE=1024
-
-prepare_test()
-{
-    umount_jfs /tmp/jfs $META_URL
-    python3 .github/scripts/flush_meta.py $META_URL
-    rm -rf /var/jfs/myjfs || true
-    rm -rf /var/jfsCache/myjfs || true
-}
-
-mount_jfsCache1(){
-    /etc/init.d/redis-server start
-    timeout 30s bash -c 'until nc -zv localhost 6379; do sleep 1; done'
-    umount -l /var/jfsCache1 || true
-    rm -rf /var/jfsCache1
-    redis-cli flushall
-    rm -rf /var/jfs/test
-    ./juicefs format "redis://localhost/1?read-timeout=3&write-timeout=1&max-retry-backoff=3" test --trash-days 0
-    ./juicefs mount redis://localhost/1 /var/jfsCache1 -d --log /tmp/juicefs.log
-    trap "echo umount /var/jfsCache1 && umount -l /var/jfsCache1" EXIT
-}
-
-check_evict_log(){
-    log=$1
-    result=$(cat $log |  sed 's/.*(\([0-9]*\.[0-9]*%\)).*/\1/' | sed 's/%//')
-    if (( $(echo "$result > 1" | bc -l) )); then
-        echo "cache ratio should be less than 1% after evict, actual is $result"
-        exit 1
-    fi
-}
-
-check_warmup_log(){
-    log=$1
-    ratio=$2
-    result=$(cat $log |  sed 's/.*(\([0-9]*\.[0-9]*%\)).*/\1/' | sed 's/%//')
-    if (( $(echo "$result < $ratio" | bc -l) )); then
-        echo "cache ratio should be more than 98% after warmup, actual is $result"
-        exit 1
-    fi
-}
-
-
-check_cache_distribute() {
-    max_total_size=$(echo "$1 * 1024" | bc | awk '{printf "%.0f", $1}')
-    echo check_cache_distribute, max_total_size is $max_total_size
-    shift
-    total_weight=0
-    declare -A weights
-    declare -A sizes
-    # Parse directory names and weights
-    for arg in "$@"; do
-        dir=$(echo "$arg" | awk -F: '{print $1}')
-        weight=$(echo "$arg" | awk -F: '{print $2}')
-        if [[ -z $weight ]]; then
-            weight=1
-        fi
-        weights["$dir"]=$weight
-        total_weight=$((total_weight + weight))
-    done
-    
-    # Calculate total size and sizes of each directory
-    for dir in "${!weights[@]}"; do
-        echo dir is $dir
-        du -sh "$dir" || true
-        size=$(du -s "$dir" | awk '{print $1}')
-        echo size is $size
-        sizes["$dir"]=$size
-    done
-    
-    # Check if total size exceeds max limit
-    total_size=0
-    for dir in "${!sizes[@]}"; do
-        size=${sizes["$dir"]}
-        total_size=$((total_size + size))
-    done
-    echo "total size is $total_size, max_total_size is $max_total_size"
-    if [[ $total_size -gt $((max_total_size + max_total_size/10)) ]]; then
-        echo "Total size of directories exceeds max limit"
-        return 1
-    fi
-    
-    # Check if each directory is evenly distributed based on its weight
-    for dir in "${!sizes[@]}"; do
-        size=${sizes["$dir"]}
-        weight=${weights["$dir"]}
-        avg_size=$((total_size * weight / total_weight))
-        min_size=$((avg_size * 5 / 10))
-        max_size=$((avg_size * 20 / 10))
-        
-        if [[ $size -lt $min_size || $size -gt $max_size ]]; then
-            echo "$dir is not evenly distributed, size: $size, weight: $weight, ave_size: $avg_size, min_size: $min_size, max_size: $max_size"
-            exit 1
-        else
-            echo "$dir is evenly distributed"
-        fi
-    done
-}
-
-test_disk_failover()
-{
-    prepare_test
-    mount_jfsCache1
-    rm -rf /var/log/juicefs.log
-    rm -rf /var/jfsCache2 /var/jfsCache3
-    ./juicefs format $META_URL myjfs --trash-days 0
-    JFS_MAX_DURATION_TO_DOWN=10s JFS_MAX_IO_DURATION=3s ./juicefs mount $META_URL /tmp/jfs -d --cache-dir=/var/jfsCache1:/var/jfsCache2:/var/jfsCache3 --io-retries 1
-    dd if=/dev/urandom of=/tmp/test_failover bs=1M count=$TEST_FILE_SIZE
-    cp /tmp/test_failover /tmp/jfs/test_failover
-    /etc/init.d/redis-server stop
-    ./juicefs warmup /tmp/jfs/test_failover
-    ./juicefs warmup --check /tmp/jfs 2>&1 | tee check.log
-    check_warmup_log check.log 50
-    wait_disk_down 60
-    ./juicefs warmup /tmp/jfs/test_failover
-    ./juicefs warmup --check /tmp/jfs 2>&1 | tee check.log
-    check_warmup_log check.log 98
-    check_cache_distribute $TEST_FILE_SIZE /var/jfsCache2 /var/jfsCache3
-    echo stop minio && docker stop minio
-    compare_md5sum /tmp/test_failover /tmp/jfs/test_failover
-    docker start minio && sleep 3
-}
-
-wait_disk_down()
-{
-    timeout=$1
-    for i in $(seq 1 $timeout); do
-        if grep -q "state change from unstable to down" /var/log/juicefs.log; then
-            echo "state changed from unstable to down"
-            return
-        else
-            echo "\rWait for state change to down, $i"
-            sleep 1
-            count=$((count+1))
-        fi
-    done
-    echo "Wait for state change to down timeout" && exit 1
-}   
-
-source .github/scripts/common/run_test.sh && run_test $@
-
diff --git a/.github/scripts/command/dump_load.sh b/.github/scripts/command/dump_load.sh
new file mode 100755
index 00000000..524965a9
--- /dev/null
+++ b/.github/scripts/command/dump_load.sh
@@ -0,0 +1,262 @@
+#!/bin/bash -ex
+source .github/scripts/common/common.sh
+
+[[ -z "$META" ]] && META=sqlite3
+source .github/scripts/start_meta_engine.sh
+start_meta_engine $META
+META_URL=$(get_meta_url $META)
+META_URL2=$(get_meta_url2 $META)
+[[ -z "$SEED" ]] && SEED=$(date +%s)
+HEARTBEAT_INTERVAL=2
+DIR_QUOTA_FLUSH_INTERVAL=4
+# [[ -z "$SEED" ]] && SEED=1711594639
+[[ -z "$BINARY" ]] && BINARY=false
+[[ -z "$FAST" ]] && FAST=false
+
+trap "echo random seed is $SEED" EXIT
+
+if ! docker ps | grep -q minio; then
+    docker run -d -p 9000:9000 --name minio \
+            -e "MINIO_ACCESS_KEY=minioadmin" \
+            -e "MINIO_SECRET_KEY=minioadmin" \
+            -v /tmp/data:/data \
+            -v /tmp/config:/root/.minio \
+            minio/minio server /data
+fi
+[[ ! -f /usr/local/bin/mc ]] && wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc -O /usr/local/bin/mc && chmod +x /usr/local/bin/mc
+sleep 3s
+mc alias set myminio http://localhost:9000 minioadmin minioadmin
+python3 -c "import xattr" || sudo pip install xattr
+
+test_dump_load_sustained_file(){
+    prepare_test
+    ./juicefs format $META_URL myjfs --trash-days 0
+    ./juicefs mount -d $META_URL /jfs
+    file_count=100
+    for i in $(seq 1 $file_count); do
+        touch /jfs/file$i
+        exec {fd}<>/jfs/file$i
+        echo fd is $fd
+        fds[$i]=$fd
+        rm /jfs/file$i
+    done
+    ./juicefs dump $META_URL dump.json $(get_dump_option)
+    for i in $(seq 1 $file_count); do
+        fd=${fds[$i]}
+        exec {fd}>&-
+    done
+    if [[ "$BINARY" == "true" ]]; then
+        sustained=$(./juicefs load dump.json --binary --stat | grep sustained | awk -F"|" '{print $2}')
+    else
+        sustained=$(jq '.Sustained[].inodes | length' dump.json)
+    fi
+    echo "sustained file count: $sustained"
+    # TODO： uncomment this line 
+    # [[ "$sustained" -eq "$file_count" ]] || (echo "sustained file count($sustained) should be $file_count" && exit 1)
+    umount_jfs /jfs $META_URL
+    python3 .github/scripts/flush_meta.py $META_URL
+    ./juicefs load $META_URL dump.json $(get_load_option)
+    ./juicefs mount -d $META_URL /jfs 
+}
+
+test_dump_load_with_copy_file_range(){
+    prepare_test
+    ./juicefs format $META_URL myjfs
+    ./juicefs mount -d $META_URL /jfs
+    rm -rf /tmp/test
+    dd if=/dev/zero of=/tmp/test bs=1M count=1024
+    cp /tmp/test /jfs/test
+    node .github/scripts/copyFile.js /jfs/test /jfs/test1
+    ./juicefs dump $META_URL dump.json $(get_dump_option)
+    umount_jfs /jfs $META_URL
+    python3 .github/scripts/flush_meta.py $META_URL
+    ./juicefs load $META_URL dump.json $(get_load_option)
+    ./juicefs mount -d $META_URL /jfs
+    compare_md5sum /tmp/test /jfs/test1
+}
+
+test_dump_load_with_quota(){
+    prepare_test
+    ./juicefs format $META_URL myjfs 
+    ./juicefs mount -d $META_URL /jfs --heartbeat $HEARTBEAT_INTERVAL
+    mkdir -p /jfs/d
+    ./juicefs quota set $META_URL --path /d --inodes 1000 --capacity 1
+    ./juicefs dump --log-level error $META_URL $(get_dump_option) > dump.json
+    umount_jfs /jfs $META_URL
+    python3 .github/scripts/flush_meta.py $META_URL
+    ./juicefs load $META_URL dump.json $(get_load_option)
+    ./juicefs mount $META_URL /jfs -d --heartbeat $HEARTBEAT_INTERVAL
+    ./juicefs quota get $META_URL --path /d
+    dd if=/dev/zero of=/jfs/d/test1 bs=1G count=1
+    sleep $DIR_QUOTA_FLUSH_INTERVAL
+    echo a | tee -a /jfs/d/test1 2>error.log && echo "write should fail on out of space" && exit 1 || true
+    grep "Disk quota exceeded" error.log || (echo "grep failed" && exit 1)
+}
+
+test_dump_load_with_iflag(){
+    prepare_test
+    ./juicefs format $META_URL myjfs
+    ./juicefs mount -d $META_URL /jfs --enable-ioctl
+    echo "hello" > /jfs/hello.txt
+    chattr +i /jfs/hello.txt
+    ./juicefs dump $META_URL dump.json $(get_dump_option)
+    umount_jfs /jfs $META_URL
+    python3 .github/scripts/flush_meta.py $META_URL
+    ./juicefs load $META_URL dump.json $(get_load_option)
+    ./juicefs mount -d $META_URL /jfs --enable-ioctl
+    echo "hello" > /jfs/hello.txt && echo "write should fail" && exit 1 || true
+    chattr -i /jfs/hello.txt
+    echo "world" > /jfs/hello.txt
+    cat /jfs/hello.txt | grep world
+}
+
+test_dump_load_with_keep_secret_key()
+{
+    option=$@
+    prepare_test
+    ./juicefs format $META_URL myjfs --storage minio --bucket http://localhost:9000/test --access-key minioadmin --secret-key minioadmin
+    ./juicefs dump --keep-secret-key $META_URL dump.json $(get_dump_option)
+    python3 .github/scripts/flush_meta.py $META_URL
+    ./juicefs load $META_URL dump.json $(get_load_option)
+    ./juicefs mount -d $META_URL /jfs
+    echo "hello" > /jfs/hello.txt
+    cat /jfs/hello.txt | grep hello
+
+    umount_jfs /jfs $META_URL
+    ./juicefs dump $META_URL dump.json $(get_dump_option)
+    python3 .github/scripts/flush_meta.py $META_URL
+    ./juicefs load $META_URL dump.json $(get_load_option)
+    ./juicefs mount -d $META_URL /jfs && echo "mount should fail" && exit 1 || true
+    ./juicefs config --secret-key minioadmin $META_URL
+    ./juicefs mount -d $META_URL /jfs
+    echo "hello" > /jfs/hello.txt
+    cat /jfs/hello.txt | grep hello
+}
+
+test_load_encrypted_meta_backup()
+{
+    prepare_test
+    [[ ! -f my-priv-key.pem ]] && openssl genrsa -out my-priv-key.pem -aes256 -passout pass:12345678 2048
+    export JFS_RSA_PASSPHRASE=12345678
+    ./juicefs format $META_URL myjfs --encrypt-rsa-key my-priv-key.pem
+    ./juicefs mount -d $META_URL /jfs
+    SEED=$SEED LOG_LEVEL=WARNING MAX_EXAMPLE=50 STEP_COUNT=50 PROFILE=generate ROOT_DIR1=/jfs/test ROOT_DIR2=/tmp/test python3 .github/scripts/hypo/fs.py || true
+    umount /jfs
+    SKIP_BACKUP_META_CHECK=true ./juicefs mount -d --backup-meta 10s $META_URL /jfs
+    sleep 10s
+    backup_file=$(ls -l /var/jfs/myjfs/meta/ |tail -1 | awk '{print $NF}')
+    backup_path=/var/jfs/myjfs/meta/$backup_file
+    ls -l $backup_path
+
+    ./juicefs load sqlite3://test2.db $backup_path --encrypt-rsa-key my-priv-key.pem --encrypt-algo aes256gcm-rsa
+    ./juicefs mount -d sqlite3://test2.db /jfs2
+    diff -ur /jfs/test /jfs2/test --no-dereference
+    umount_jfs /jfs2 sqlite3://test2.db
+    rm test2.db -rf
+}
+
+test_dump_load_with_random_test()
+{
+    prepare_test
+    ./juicefs format $META_URL myjfs --enable-acl
+    ./juicefs mount -d $META_URL /jfs 
+    ./random-test runOp -baseDir /jfs/test -files 500000 -ops 5000000 -threads 50 -dirSize 10 -duration 30s -createOp 30,uniform -deleteOp 5,end --linkOp 10,uniform --removeLinkOp 1,end --symlinkOp 20,uniform --setXattrOp 10,uniform --truncateOp 10,uniform    
+    ./juicefs dump $META_URL dump.json $(get_dump_option)
+    create_database $META_URL2
+    ./juicefs load $META_URL2 dump.json $(get_load_option)
+    ./juicefs dump $META_URL2 dump2.json $(get_dump_option)
+    ./juicefs mount -d $META_URL2 /jfs2
+    diff -ur /jfs/test /jfs2/test --no-dereference
+    diff -ur /jfs/.trash /jfs2/.trash --no-dereference
+    # compare_stat_acl_xattr /jfs/test /jfs2/test
+    umount_jfs /jfs2 $META_URL2
+    ./juicefs status $META_URL2 && UUID=$(./juicefs status $META_URL2 | grep UUID | cut -d '"' -f 4)
+    ./juicefs destroy --yes $META_URL2 $UUID
+}
+
+test_dump_load_with_fsrand()
+{
+    prepare_test
+    ./juicefs format $META_URL myjfs --trash-days 0 --enable-acl
+    ./juicefs mount -d $META_URL /jfs --enable-xattr
+    rm -rf /tmp/test
+    SEED=$SEED LOG_LEVEL=WARNING MAX_EXAMPLE=30 STEP_COUNT=20 PROFILE=generate ROOT_DIR1=/jfs/test ROOT_DIR2=/tmp/test python3 .github/scripts/hypo/fs.py || true    
+    ./juicefs dump $META_URL dump.json $(get_dump_option)
+    create_database $META_URL2
+    ./juicefs load $META_URL2 dump.json $(get_load_option)
+    ./juicefs dump $META_URL2 dump2.json $(get_dump_option)
+    # if [[ "$BINARY" == "false" ]]; then
+    #     compare_dump_json
+    # fi
+    ./juicefs mount -d $META_URL2 /jfs2
+    diff -ur /jfs/test /jfs2/test --no-dereference
+    compare_stat_acl_xattr /jfs/test /jfs2/test
+    umount_jfs /jfs2 $META_URL2
+    ./juicefs status $META_URL2 && UUID=$(./juicefs status $META_URL2 | grep UUID | cut -d '"' -f 4)
+    ./juicefs destroy --yes $META_URL2 $UUID
+}
+
+compare_dump_json(){
+    cp dump.json dump.json.bak
+    cp dump2.json dump2.json.bak
+    sed -i '/usedSpace/d' dump*.json.bak
+    sed -i '/usedInodes/d' dump*.json.bak
+    sed -i '/nextInodes/d' dump*.json.bak
+    sed -i '/nextChunk/d' dump*.json.bak
+    sed -i '/nextTrash/d' dump*.json.bak
+    sed -i '/nextSession/d' dump*.json.bak
+    sed -i 's/"inode":[0-9]\+/"inode":0/g' dump*.json.bak
+    diff -ur dump.json.bak dump2.json.bak
+}
+
+compare_stat_acl_xattr(){
+    dir1=$1
+    dir2=$2
+    files1=($(find "$dir1" -type f -o -type d -exec stat -c "%n" {} + | sort))
+    files2=($(find "$dir2" -type f -o -type d -exec stat -c "%n" {} + | sort))
+    [[ ${#files1[@]} -ne ${#files2[@]} ]] && echo "compare_stat_acl: number of files differs" && exit 1
+    for i in "${!files1[@]}"; do
+        stat1=$(stat -c "%F %a %s %h %U %G" "${files1[$i]}")
+        stat2=$(stat -c "%F %a %s %h %U %G" "${files2[$i]}")
+        acl1=$(getfacl -p "${files1[$i]}" | tail -n +2)
+        acl2=$(getfacl -p "${files2[$i]}" | tail -n +2)
+        xattr1=$(getfattr -d -m . -e hex "${files1[$i]}" 2>/dev/null | tail -n +2 | sort)
+        xattr2=$(getfattr -d -m . -e hex "${files2[$i]}" 2>/dev/null | tail -n +2 | sort)
+        [[ "$stat1" != "$stat2" ]] && echo "compare_stat_acl: stat for ${files1[$i]} and ${files2[$i]} differs" && echo $stat1 && echo $stat2 && exit 1
+        [[ "$acl1" != "$acl2" ]] && echo "compare_stat_acl: ACLs for ${files1[$i]} and ${files2[$i]} differs" && echo $acl1 && echo $acl2 && exit 1
+        [[ "$xattr1" != "$xattr2" ]] && echo "compare_stat_acl: xattrs for ${files1[$i]} and ${files2[$i]} differs" && echo $xattr1 && echo $xattr2 && exit 1
+
+    done
+    echo "compare_stat_acl: ACLs and stats are the same"
+}
+
+get_dump_option(){
+    if [[ "$BINARY" == "true" ]]; then 
+        option="--binary"
+    elif [[ "$FAST" == "true" ]]; then
+        option="--fast"
+    else
+        option=""
+    fi
+    echo $option
+}
+
+get_load_option(){
+    if [[ "$BINARY" == "true" ]]; then 
+        option="--binary"
+    else
+        option=""
+    fi
+    echo $option
+}
+
+prepare_test(){
+    umount_jfs /jfs $META_URL
+    umount_jfs /jfs2 sqlite3://test2.db
+    python3 .github/scripts/flush_meta.py $META_URL
+    rm test2.db -rf 
+    rm -rf /var/jfs/myjfs || true
+    mc rm --force --recursive myminio/test || true
+}
+
+source .github/scripts/common/run_test.sh && run_test $@
diff --git a/.github/scripts/command/load_dump_bench.sh b/.github/scripts/command/dump_load_bench.sh
similarity index 100%
rename from .github/scripts/command/load_dump_bench.sh
rename to .github/scripts/command/dump_load_bench.sh
diff --git a/.github/scripts/command/info.sh b/.github/scripts/command/info.sh
index 435c8bd5..6a710321 100755
--- a/.github/scripts/command/info.sh
+++ b/.github/scripts/command/info.sh
@@ -12,7 +12,7 @@ test_info_big_file(){
     prepare_test
     ./juicefs format $META_URL myjfs
     ./juicefs mount -d $META_URL /jfs
-    dd if=/dev/urandom of=/jfs/bigfile bs=16M count=1024
+    dd if=/dev/zero of=/jfs/bigfile bs=1M count=4096
     ./juicefs info /jfs/bigfile
     ./juicefs rmr /jfs/bigfile
     df -h /jfs
diff --git a/.github/scripts/command/load_dump.sh b/.github/scripts/command/load_dump.sh
deleted file mode 100755
index 3ea9cc5f..00000000
--- a/.github/scripts/command/load_dump.sh
+++ /dev/null
@@ -1,190 +0,0 @@
-#!/bin/bash -e
-source .github/scripts/common/common.sh
-
-[[ -z "$META" ]] && META=sqlite3
-source .github/scripts/start_meta_engine.sh
-start_meta_engine $META
-META_URL=$(get_meta_url $META)
-[[ -z "$SEED" ]] && SEED=$(date +%s)
-# [[ -z "$SEED" ]] && SEED=1711594639
-
-trap "echo random seed is $SEED" EXIT
-
-if ! docker ps | grep -q minio; then
-    docker run -d -p 9000:9000 --name minio \
-            -e "MINIO_ACCESS_KEY=minioadmin" \
-            -e "MINIO_SECRET_KEY=minioadmin" \
-            -v /tmp/data:/data \
-            -v /tmp/config:/root/.minio \
-            minio/minio server /data
-fi
-[[ ! -f /usr/local/bin/mc ]] && wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc -O /usr/local/bin/mc && chmod +x /usr/local/bin/mc
-sleep 3s
-mc alias set myminio http://localhost:9000 minioadmin minioadmin
-python3 -c "import xattr" || sudo pip install xattr
-
-test_dump_load_with_iflag(){
-    do_dump_load_with_iflag
-}
-
-test_dump_load_with_iflag_binary(){
-    do_dump_load_with_iflag --binary
-}
-
-do_dump_load_with_iflag(){
-    option=$@
-    prepare_test
-    ./juicefs format $META_URL myjfs
-    ./juicefs mount -d $META_URL /jfs --enable-ioctl
-    echo "hello" > /jfs/hello.txt
-    chattr +i /jfs/hello.txt
-    ./juicefs dump $META_URL dump.json $option
-    umount_jfs /jfs $META_URL
-    python3 .github/scripts/flush_meta.py $META_URL
-    ./juicefs load $META_URL dump.json $option
-    ./juicefs mount -d $META_URL /jfs --enable-ioctl
-    echo "hello" > /jfs/hello.txt && echo "write should fail" && exit 1 || true
-    chattr -i /jfs/hello.txt
-    echo "world" > /jfs/hello.txt
-    cat /jfs/hello.txt | grep world
-}
-
-test_dump_load_with_keep_secret_key(){
-    do_dump_load_with_keep_secret_key
-}
-
-test_dump_load_with_keep_secret_key_in_binary(){
-    do_dump_load_with_keep_secret_key --binary
-}
-
-do_dump_load_with_keep_secret_key()
-{
-    option=$@
-    prepare_test
-    ./juicefs format $META_URL myjfs --storage minio --bucket http://localhost:9000/test --access-key minioadmin --secret-key minioadmin
-    ./juicefs dump --keep-secret-key $META_URL dump.json $option
-    python3 .github/scripts/flush_meta.py $META_URL
-    ./juicefs load $META_URL dump.json $option
-    ./juicefs mount -d $META_URL /jfs
-    echo "hello" > /jfs/hello.txt
-    cat /jfs/hello.txt | grep hello
-
-    umount_jfs /jfs $META_URL
-    ./juicefs dump $META_URL dump.json $option
-    python3 .github/scripts/flush_meta.py $META_URL
-    ./juicefs load $META_URL dump.json $option
-    ./juicefs mount -d $META_URL /jfs && echo "mount should fail" && exit 1 || true
-    ./juicefs config --secret-key minioadmin $META_URL
-    ./juicefs mount -d $META_URL /jfs
-    echo "hello" > /jfs/hello.txt
-    cat /jfs/hello.txt | grep hello
-}
-
-
-test_dump_load_with_trash_enable(){
-    do_dump_load_with_fsrand 1
-}
-test_dump_load_with_trash_disable(){
-    do_dump_load_with_fsrand 0
-}
-
-do_dump_load_with_fsrand(){
-    trash_days=$1
-    prepare_test
-    ./juicefs format $META_URL myjfs --trash-days $trash_days --enable-acl
-    ./juicefs mount -d $META_URL /jfs --enable-xattr
-    SEED=$SEED LOG_LEVEL=WARNING MAX_EXAMPLE=30 STEP_COUNT=20 PROFILE=generate ROOT_DIR1=/jfs/fsrand ROOT_DIR2=/tmp/fsrand python3 .github/scripts/hypo/fs.py || true
-    do_dump_load_and_compare 
-    do_dump_load_and_compare --binary
-    do_dump_load_and_compare --fast
-    do_dump_load_and_compare --skip-trash
-    do_dump_load_and_compare --fast --skip-trash
-}
-
-do_dump_load_and_compare()
-{
-    option=$@
-    echo option is $option
-    ./juicefs dump $META_URL dump.json $option
-    rm -rf test2.db 
-    if [[ "$option" == *"--binary"* ]]; then
-        ./juicefs load sqlite3://test2.db dump.json $option
-    else
-        ./juicefs load sqlite3://test2.db dump.json
-    fi
-    ./juicefs dump sqlite3://test2.db dump2.json $option
-    # if [[ "$option" != *"--binary"* ]]; then
-    #     compare_dump_json
-    # fi
-    ./juicefs mount -d sqlite3://test2.db /jfs2
-    diff -ur /jfs/fsrand /jfs2/fsrand --no-dereference
-    compare_stat_acl_xattr /jfs/fsrand /jfs2/fsrand
-    umount /jfs2
-}
-
-compare_dump_json(){
-    cp dump.json dump.json.bak
-    cp dump2.json dump2.json.bak
-    sed -i '/usedSpace/d' dump*.json.bak
-    sed -i '/usedInodes/d' dump*.json.bak
-    sed -i '/nextInodes/d' dump*.json.bak
-    sed -i '/nextChunk/d' dump*.json.bak
-    sed -i '/nextTrash/d' dump*.json.bak
-    sed -i '/nextSession/d' dump*.json.bak
-    sed -i 's/"inode":[0-9]\+/"inode":0/g' dump*.json.bak
-    diff -ur dump.json.bak dump2.json.bak
-}
-
-compare_stat_acl_xattr(){
-    dir1=$1
-    dir2=$2
-    files1=($(find "$dir1" -type f -o -type d -exec stat -c "%n" {} + | sort))
-    files2=($(find "$dir2" -type f -o -type d -exec stat -c "%n" {} + | sort))
-    [[ ${#files1[@]} -ne ${#files2[@]} ]] && echo "compare_stat_acl: number of files differs" && exit 1
-    for i in "${!files1[@]}"; do
-        stat1=$(stat -c "%F %a %s %h %U %G" "${files1[$i]}")
-        stat2=$(stat -c "%F %a %s %h %U %G" "${files2[$i]}")
-        acl1=$(getfacl -p "${files1[$i]}" | tail -n +2)
-        acl2=$(getfacl -p "${files2[$i]}" | tail -n +2)
-        xattr1=$(getfattr -d -m . -e hex "${files1[$i]}" 2>/dev/null | tail -n +2 | sort)
-        xattr2=$(getfattr -d -m . -e hex "${files2[$i]}" 2>/dev/null | tail -n +2 | sort)
-        [[ "$stat1" != "$stat2" ]] && echo "compare_stat_acl: stat for ${files1[$i]} and ${files2[$i]} differs" && echo $stat1 && echo $stat2 && exit 1
-        [[ "$acl1" != "$acl2" ]] && echo "compare_stat_acl: ACLs for ${files1[$i]} and ${files2[$i]} differs" && echo $acl1 && echo $acl2 && exit 1
-        [[ "$xattr1" != "$xattr2" ]] && echo "compare_stat_acl: xattrs for ${files1[$i]} and ${files2[$i]} differs" && echo $xattr1 && echo $xattr2 && exit 1
-
-    done
-    echo "compare_stat_acl: ACLs and stats are the same"
-}
-
-test_load_encrypted_meta_backup()
-{
-    prepare_test
-    [[ ! -f my-priv-key.pem ]] && openssl genrsa -out my-priv-key.pem -aes256 -passout pass:12345678 2048
-    export JFS_RSA_PASSPHRASE=12345678
-    ./juicefs format $META_URL myjfs --encrypt-rsa-key my-priv-key.pem
-    ./juicefs mount -d $META_URL /jfs
-    SEED=$SEED LOG_LEVEL=WARNING MAX_EXAMPLE=50 STEP_COUNT=50 PROFILE=generate ROOT_DIR1=/jfs/fsrand ROOT_DIR2=/tmp/fsrand python3 .github/scripts/hypo/fs.py || true
-    umount /jfs
-    SKIP_BACKUP_META_CHECK=true ./juicefs mount -d --backup-meta 10s $META_URL /jfs
-    sleep 10s
-    backup_file=$(ls -l /var/jfs/myjfs/meta/ |tail -1 | awk '{print $NF}')
-    backup_path=/var/jfs/myjfs/meta/$backup_file
-    ls -l $backup_path
-
-    ./juicefs load sqlite3://test2.db $backup_path --encrypt-rsa-key my-priv-key.pem --encrypt-algo aes256gcm-rsa
-    ./juicefs mount -d sqlite3://test2.db /jfs2
-    diff -ur /jfs/fsrand /jfs2/fsrand --no-dereference
-    umount_jfs /jfs2 sqlite3://test2.db
-    rm test2.db -rf
-}
-
-prepare_test(){
-    umount_jfs /jfs $META_URL
-    umount_jfs /jfs2 sqlite3://test2.db
-    python3 .github/scripts/flush_meta.py $META_URL
-    rm test2.db -rf 
-    rm -rf /var/jfs/myjfs || true
-    mc rm --force --recursive myminio/test || true
-}
-
-source .github/scripts/common/run_test.sh && run_test $@
diff --git a/.github/scripts/copyFile.js b/.github/scripts/copyFile.js
new file mode 100644
index 00000000..d6d8934d
--- /dev/null
+++ b/.github/scripts/copyFile.js
@@ -0,0 +1,19 @@
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+if (process.argv.length !== 4) {
+  console.error('Usage: node copyFile.js <sourceFile> <destinationFile>');
+  process.exit(1);
+}
+
+const sourceFile = path.resolve(process.argv[2]);
+const destinationFile = path.resolve(process.argv[3]);
+
+fs.copyFile(sourceFile, destinationFile, async (err) => {
+  if (err) {
+    console.error('Error copying file:', err);
+    process.exit(1);
+  }
+  console.log('File copied successfully.');
+});
\ No newline at end of file
diff --git a/.github/scripts/hypo/fs.py b/.github/scripts/hypo/fs.py
index 9394e50d..ee84dc6c 100644
--- a/.github/scripts/hypo/fs.py
+++ b/.github/scripts/hypo/fs.py
@@ -44,7 +44,7 @@ class JuicefsMachine(RuleBasedStateMachine):
     if os.getenv('EXCLUDE_RULES'):
         EXCLUDE_RULES = os.getenv('EXCLUDE_RULES').split(',')
     else:
-        EXCLUDE_RULES = ['clone_cp_file', 'clone_cp_dir', 'loop_symlink', 'symlink', 'readlines', 'readline']
+        EXCLUDE_RULES = ['readlines', 'readline']
         # EXCLUDE_RULES = ['rebalance_dir', 'rebalance_file', 'clone_cp_file', 'clone_cp_dir', 'loop_symlink', 'hardlink', 'rename_dir', 'chown']
     ROOT_DIR1=os.environ.get('ROOT_DIR1', '/tmp/fsrand')
     ROOT_DIR2=os.environ.get('ROOT_DIR2', '/tmp/jfs/fsrand')
diff --git a/.github/scripts/hypo/sync.py b/.github/scripts/hypo/sync.py
index 1ce0dac2..731f0180 100644
--- a/.github/scripts/hypo/sync.py
+++ b/.github/scripts/hypo/sync.py
@@ -69,13 +69,11 @@ class SyncMachine(RuleBasedStateMachine):
     @rule(target=Files, 
           parent = Folders.filter(lambda x: x != multiple()), 
           file_name = st_entry_name, 
-          mode = st_open_mode, 
-          content = st_content, 
           umask = st_umask, 
             )
-    def create_file(self, parent, file_name, content, mode='x', user='root', umask=0o022):
-        result1 = self.fsop1.do_create_file(parent, file_name, mode, content, user, umask)
-        result2 = self.fsop2.do_create_file(parent, file_name, mode, content, user, umask)
+    def create_file(self, parent, file_name, content='s', mode='x', user='root', umask=0o022):
+        result1 = self.fsop1.do_create_file(parent=parent, file_name=file_name, mode=mode, content=content, user=user, umask=umask)
+        result2 = self.fsop2.do_create_file(parent=parent, file_name=file_name, mode=mode, content=content, user=user, umask=umask)
         assert self.equal(result1, result2), f'\033[31mcreate_file:\nresult1 is {result1}\nresult2 is {result2}\033[0m'
         if isinstance(result1, Exception):
             return multiple()
diff --git a/.github/scripts/start_meta_engine.sh b/.github/scripts/start_meta_engine.sh
index 65d3dbb4..95c6698f 100755
--- a/.github/scripts/start_meta_engine.sh
+++ b/.github/scripts/start_meta_engine.sh
@@ -228,7 +228,7 @@ get_meta_url2(){
     elif [ "$meta" == "sqlite3" ]; then
         meta_url="sqlite3://test2.db"
     elif [ "$meta" == "tikv" ]; then
-        meta_url="tikv://127.0.0.1:2379/jfs"
+        meta_url="tikv://127.0.0.1:2379/jfs2"
     elif [ "$meta" == "badger" ]; then
         meta_url="badger:///tmp/test2"
     elif [ "$meta" == "mariadb" ]; then
@@ -238,7 +238,7 @@ get_meta_url2(){
     elif [ "$meta" == "etcd" ]; then
         meta_url="etcd://localhost:2379/test2"
     elif [ "$meta" == "fdb" ]; then
-        meta_url="fdb:///home/runner/fdb.cluster?prefix=kfs"
+        meta_url="fdb:///home/runner/fdb.cluster?prefix=jfs2"
     elif [ "$meta" == "ob" ]; then
         meta_url="mysql://root:@\\(127.0.0.1:2881\\)/test2"
     else
diff --git a/.github/scripts/sync/sync_cluster.sh b/.github/scripts/sync/sync_cluster.sh
index 8a16f796..bcbfa642 100755
--- a/.github/scripts/sync/sync_cluster.sh
+++ b/.github/scripts/sync/sync_cluster.sh
@@ -81,15 +81,19 @@ test_sync_without_mount_point2(){
     ./mc cp -r data myminio/data
     
     # (./mc rb myminio/data1 > /dev/null 2>&1 --force || true) && ./mc mb myminio/data1
+    set -o pipefail
     sudo -u juicedata meta_url=$META_URL ./juicefs sync -v  minio://minioadmin:minioadmin@172.20.0.1:9000/data/ jfs://meta_url/ \
          --manager-addr 172.20.0.1:8081 --worker juicedata@172.20.0.2,juicedata@172.20.0.3 \
          --list-threads 10 --list-depth 5\
          2>&1 | tee sync.log
+    set +o pipefail
     check_sync_log $file_count
+    set -o pipefail
     sudo -u juicedata meta_url=$META_URL ./juicefs sync -v  minio://minioadmin:minioadmin@172.20.0.1:9000/data/ jfs://meta_url/ \
          --manager-addr 172.20.0.1:8081 --worker juicedata@172.20.0.2,juicedata@172.20.0.3 \
          --list-threads 10 --list-depth 5 --check-all \
          2>&1 | tee sync.log
+    set +o pipefail
     ./juicefs mount -d $META_URL /jfs
     diff data/ /jfs/data/
     ./mc rm -r --force myminio/data
@@ -120,7 +124,7 @@ skip_test_sync_between_oss(){
 }
 
 check_sync_log(){
-    grep "<FATAL>" sync.log && exit 1 || true
+    grep "panic:\|<FATAL>" sync.log && echo "panic or fatal in sync.log" && exit 1 || true
     file_count=$1
     if tail -1 sync.log | grep -q "close session"; then
       file_copied=$(tail -n 3 sync.log | head -n 1  | sed 's/.*copied: \([0-9]*\).*/\1/' )
diff --git a/.github/workflows/cache.yml b/.github/workflows/cache.yml
new file mode 100644
index 00000000..148084b4
--- /dev/null
+++ b/.github/workflows/cache.yml
@@ -0,0 +1,77 @@
+name: "cache"
+
+on:
+  push:
+    branches:
+      - 'main'
+      - 'release-**'
+    paths:
+      - '**/cache.yml'
+      - '**/cache.sh'
+  pull_request:
+    branches:
+      - 'main'
+      - 'release-**'
+    paths:
+      - '**/cache.yml'
+      - '**/cache.sh'
+  schedule:
+    - cron:  '30 20 * * *'
+  workflow_dispatch:
+
+jobs:
+  cache:
+    timeout-minutes: 60
+    runs-on: ubuntu-20.04
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 1
+
+      - name: mount coverage dir
+        timeout-minutes: 5
+        uses: ./.github/actions/mount-coverage-dir
+        with:
+          mount_point: cover
+          access_key: ${{ secrets.CI_COVERAGE_AWS_AK }}
+          secret_key: ${{ secrets.CI_COVERAGE_AWS_SK }}
+          token: ${{ secrets.CI_COVERAGE_AWS_TOKEN }}
+
+      - name: Build
+        timeout-minutes: 10
+        uses: ./.github/actions/build
+
+      - name: Test
+        run: |
+          sudo GOCOVERDIR=$(pwd)/cover .github/scripts/cache.sh
+
+      - name: Check juicefs log
+        if: always()
+        run: |
+          sudo .github/scripts/check_juicefs_log.sh
+      
+      - name: Check /tmp/juicefs.log
+        if: always()
+        run: |
+          sudo tail -n 1000 /tmp/juicefs.log
+
+      - name: upload coverage report
+        timeout-minutes: 5
+        continue-on-error: true
+        uses: ./.github/actions/upload-coverage
+        with:
+          UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
+
+      - name: Send Slack Notification
+        if: failure()
+        uses: juicedata/slack-notify-action@main
+        with:
+          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
+          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
+      
+      - name: Setup upterm session
+        if: failure() && (github.event.inputs.debug == 'true' || github.run_attempt != 1)
+        # if: failure()
+        timeout-minutes: 60
+        uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml
index 867080b8..21c0a4e1 100644
--- a/.github/workflows/chaos.yml
+++ b/.github/workflows/chaos.yml
@@ -173,7 +173,7 @@ jobs:
             kubectl -n kube-system describe po $pod
             kubectl logs -n kube-system $pod > juicefs.log
             cat juicefs.log
-            grep "<FATAL>:" juicefs.log | grep -v forma.go && exit 1 || true
+            grep "<FATAL>:" juicefs.log | grep -v format.go && exit 1 || true
           done
 
       - name: Mount pod upgrade
@@ -222,7 +222,7 @@ jobs:
             kubectl -n kube-system describe po $pod
             kubectl logs -n kube-system $pod > juicefs.log
             cat juicefs.log
-            grep "<FATAL>:" juicefs.log | grep -v forma.go && exit 1 || true
+            grep "<FATAL>:" juicefs.log | grep -v format.go && exit 1 || true
           done
 
       - name: Check vdbench log
diff --git a/.github/workflows/command.yml b/.github/workflows/command.yml
index 9dbeaf46..7e5ffcd5 100644
--- a/.github/workflows/command.yml
+++ b/.github/workflows/command.yml
@@ -77,44 +77,48 @@ jobs:
         uses: ./.github/actions/build
   
       - name: Test Gc
-        timeout-minutes: 30
+        timeout-minutes: 10
         run: |
           sudo META=${{matrix.meta}} .github/scripts/command/gc.sh      
 
       - name: Test Config
+        timeout-minutes: 10
         run: |
           sudo META=${{matrix.meta}} .github/scripts/command/config.sh
     
       - name: Test acl
-        timeout-minutes: 30
+        timeout-minutes: 10
         run: |
           sudo META=${{matrix.meta}} .github/scripts/command/acl.sh     
 
       - name: Test Clone
-        timeout-minutes: 30
+        timeout-minutes: 10
         run: |
           sudo META=${{matrix.meta}} .github/scripts/command/clone.sh
 
       - name: Test fsck
-        timeout-minutes: 30
+        timeout-minutes: 10
         run: |
           sudo META=${{matrix.meta}} .github/scripts/command/fsck.sh 
       
       - name: Test Gateway
-        timeout-minutes: 30
+        timeout-minutes: 10
         run: |
           sudo META=${{matrix.meta}} .github/scripts/command/gateway.sh
       
       - name: Test Debug
-        timeout-minutes: 30
+        timeout-minutes: 10
         run: |
           sudo META=${{matrix.meta}} .github/scripts/command/debug.sh
       
       - name: Test Info
+        timeout-minutes: 10
         run: |
           sudo META=${{matrix.meta}} .github/scripts/command/info.sh
 
       - name: Test Format
+        if: false
+        timeout-minutes: 10
         run: |
           sudo META=${{matrix.meta}} .github/scripts/command/format.sh
   
@@ -132,50 +136,6 @@ jobs:
         uses: lhotari/action-upterm@v1
 
   command_test2:
-    needs: [build-matrix]
-    strategy:
-      fail-fast: false
-      matrix:
-        meta: ${{ fromJson(needs.build-matrix.outputs.meta_matrix) }}
-    runs-on: ubuntu-20.04
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          fetch-depth: 1
-
-      - name: Remove unused software
-        run: |
-            echo "before remove unused software"
-            sudo df -h
-            sudo rm -rf /usr/share/dotnet
-            sudo rm -rf /usr/local/lib/android
-            sudo rm -rf /opt/ghc
-            echo "after remove unused software"
-            sudo df -h
-            
-      - name: Build 
-        uses: ./.github/actions/build
-
-      - name: Test Load & Dump
-        timeout-minutes: 30
-        run: |
-          sudo META=${{matrix.meta}} .github/scripts/command/load_dump.sh
-
-      - name: Log
-        if: always()
-        run: |
-          echo "juicefs log"
-          sudo tail -n 1000 /var/log/juicefs.log
-          grep "<FATAL>:" /var/log/juicefs.log && exit 1 || true
-          
-      - name: Setup upterm session
-        if: failure() && (github.event.inputs.debug == 'true' || github.run_attempt != 1)
-        # if: failure()
-        timeout-minutes: 60
-        uses: lhotari/action-upterm@v1
-
-  command_test3:
     needs: [build-matrix]
     strategy:
       fail-fast: false
@@ -219,7 +179,7 @@ jobs:
         timeout-minutes: 60
         uses: lhotari/action-upterm@v1
 
-  command_test4:
+  command_test3:
     needs: [build-matrix]
     strategy:
       fail-fast: false
@@ -235,13 +195,6 @@ jobs:
       - name: Build 
         uses: ./.github/actions/build
 
-      - name: Test Cache
-        timeout-minutes: 30
-        run: |
-          if [[ "${{matrix.meta}}" == "mysql" || "${{matrix.meta}}" == "tikv" ]]; then
-            sudo META=${{matrix.meta}} .github/scripts/command/cache.sh
-          fi
-
       - name: Test Graceful upgrade
         timeout-minutes: 30
         run: |
@@ -266,7 +219,7 @@ jobs:
 
   success-all-test:
     runs-on: ubuntu-latest
-    needs: [command_test1, command_test2, command_test3, command_test4]
+    needs: [command_test1, command_test2, command_test3]
     if: always()
     steps:
       - uses: technote-space/workflow-conclusion-action@v3
diff --git a/.github/workflows/dump_load.yml b/.github/workflows/dump_load.yml
new file mode 100644
index 00000000..7e9bcdd0
--- /dev/null
+++ b/.github/workflows/dump_load.yml
@@ -0,0 +1,172 @@
+name: "dump_load"
+on:
+  push:
+    branches:
+      - 'main'
+      - 'release-**'
+    paths:
+      - '**/dump_load.yml'
+      - '**/dump_load.sh'
+  pull_request:
+    branches:
+      - 'main'
+      - 'release-**'
+    paths:
+      - '**/dump_load.yml'
+      - '**/dump_load.sh'
+  schedule:
+    - cron:  '0 19 * * *'
+  workflow_dispatch:
+    inputs:
+      debug:
+        type: boolean
+        description: "Run the build with tmate debugging enabled"
+        required: false
+        default: false
+
+jobs:
+  build-matrix:
+    runs-on: ubuntu-20.04
+    steps:
+      - id: set-matrix
+        run: |
+          echo "github.event_name is ${{github.event_name}}"
+          echo "GITHUB_REF_NAME is ${GITHUB_REF_NAME}"
+          if [ "${{github.event_name}}" == "schedule"  ] || [ "${{github.event_name}}" == "workflow_dispatch"  ]; then
+            echo 'meta_matrix=["sqlite3", "redis", "mysql", "tikv", "tidb", "postgres", "mariadb", "fdb"]' >> $GITHUB_OUTPUT
+          else
+            echo 'meta_matrix=["redis", "sqlite3", "tikv"]' >> $GITHUB_OUTPUT
+          fi
+    outputs:
+      meta_matrix: ${{ steps.set-matrix.outputs.meta_matrix }}
+
+  dump_load:
+    timeout-minutes: 90
+    needs: [build-matrix]
+    strategy:
+      fail-fast: false
+      matrix:
+        meta: ${{ fromJson(needs.build-matrix.outputs.meta_matrix) }}
+
+    runs-on: ubuntu-20.04
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 1
+
+      - name: mount coverage dir
+        timeout-minutes: 5
+        uses: ./.github/actions/mount-coverage-dir
+        with:
+          mount_point: cover
+          access_key: ${{ secrets.CI_COVERAGE_AWS_AK }}
+          secret_key: ${{ secrets.CI_COVERAGE_AWS_SK }}
+          token: ${{ secrets.CI_COVERAGE_AWS_TOKEN }}
+
+      - name: Set Variable
+        id: vars
+        run: |
+          if [ "${{matrix.meta}}" == "fdb" ]; then
+            echo "target=juicefs.fdb" >> $GITHUB_OUTPUT
+          else
+            echo "target=juicefs" >> $GITHUB_OUTPUT
+          fi
+
+      - name: Build
+        timeout-minutes: 10
+        uses: ./.github/actions/build
+        with: 
+          target: ${{steps.vars.outputs.target}}
+
+      - name: Install nodejs
+        run: |
+          curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
+          export NVM_DIR="$HOME/.nvm"
+          [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
+          [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
+          nvm install 22
+          nvm use 22
+          node -v 
+          nvm current
+
+      - name: Download Random Test
+        run: |
+          wget https://juicefs-com-static.oss-cn-shanghai.aliyuncs.com/random-test/random-test
+          chmod +x random-test
+
+      - name: Test Load & Dump with Binary
+        timeout-minutes: 30
+        continue-on-error: true
+        run: |
+          sudo BINARY=true GOCOVERDIR=$(pwd)/cover META=${{matrix.meta}} .github/scripts/command/dump_load.sh
+
+      - name: Test Load & Dump with Json Fast Mode
+        timeout-minutes: 30
+        run: |
+          sudo FAST=true GOCOVERDIR=$(pwd)/cover META=${{matrix.meta}} .github/scripts/command/dump_load.sh  
+
+      - name: Test Load & Dump with Json
+        timeout-minutes: 30
+        run: |
+          sudo GOCOVERDIR=$(pwd)/cover META=${{matrix.meta}} .github/scripts/command/dump_load.sh     
+
+      - name: log
+        if: always()
+        run: | 
+          tail -500 /var/log/juicefs.log
+          grep "<FATAL>:" /var/log/juicefs.log && exit 1 || true
+
+      - name: upload coverage report
+        timeout-minutes: 5
+        continue-on-error: true
+        uses: ./.github/actions/upload-coverage
+        with:
+          UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
+
+      - name: Setup upterm session
+        if: failure() && (github.event.inputs.debug == 'true' || github.run_attempt != 1)
+        # if: failure()
+        timeout-minutes: 60
+        uses: lhotari/action-upterm@v1
+
+  success-all-test:
+    runs-on: ubuntu-latest
+    needs: [dump_load]
+    if: always()
+    steps:
+      - uses: technote-space/workflow-conclusion-action@v3
+      - uses: actions/checkout@v3
+
+      - name: Check Failure
+        if: env.WORKFLOW_CONCLUSION == 'failure'
+        run: exit 1
+
+      - name: mount coverage dir
+        timeout-minutes: 5
+        uses: ./.github/actions/mount-coverage-dir
+        with:
+          mount_point: cover
+          access_key: ${{ secrets.CI_COVERAGE_AWS_AK }}
+          secret_key: ${{ secrets.CI_COVERAGE_AWS_SK }}
+          token: ${{ secrets.CI_COVERAGE_AWS_TOKEN }}
+
+      - name: upload total coverage report
+        timeout-minutes: 30
+        continue-on-error: true
+        uses: ./.github/actions/upload-total-coverage
+        with:
+          UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
+
+      - name: Send Slack Notification
+        if: failure() && github.event_name != 'workflow_dispatch' 
+        uses: juicedata/slack-notify-action@main
+        with:
+          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
+          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
+
+      - name: Success
+        if: success()
+        run: echo "All Done"
+        
\ No newline at end of file
diff --git a/.github/workflows/load.yml b/.github/workflows/dump_load_bench.yml
similarity index 73%
rename from .github/workflows/load.yml
rename to .github/workflows/dump_load_bench.yml
index d4d77b2d..8e6b36cd 100644
--- a/.github/workflows/load.yml
+++ b/.github/workflows/dump_load_bench.yml
@@ -1,19 +1,19 @@
-name: "load"
+name: "dump_load_bench"
 on:
   push:
     branches:
       - 'main'
       - 'release-**'
     paths:
-      - '**/load.yml'
-      - '**/load_dump_bench.sh'
+      - '**/dump_load_bench.yml'
+      - '**/dump_load_bench.sh'
   pull_request:
     branches:
       - 'main'
       - 'release-**'
     paths:
-      - '**/load.yml'
-      - '**/load_dump_bench.sh'
+      - '**/dump_load_bench.yml'
+      - '**/dump_load_bench.sh'
   schedule:
     - cron:  '0 19 * * *'
   workflow_dispatch:
@@ -35,12 +35,12 @@ jobs:
           if [ "${{github.event_name}}" == "schedule"  ] || [ "${{github.event_name}}" == "workflow_dispatch"  ]; then
             echo 'meta_matrix=["sqlite3", "redis", "mysql", "tikv", "tidb", "postgres", "mariadb", "fdb"]' >> $GITHUB_OUTPUT
           else
-            echo 'meta_matrix=["redis", "tikv", "mysql"]' >> $GITHUB_OUTPUT
+            echo 'meta_matrix=["redis", "sqlite3", "tikv"]' >> $GITHUB_OUTPUT
           fi
     outputs:
       meta_matrix: ${{ steps.set-matrix.outputs.meta_matrix }}
 
-  load:
+  dump_load_bench:
     timeout-minutes: 90
     needs: [build-matrix]
     strategy:
@@ -80,7 +80,18 @@ jobs:
         with: 
           target: ${{steps.vars.outputs.target}}
 
-      - name: Test dump load in binary format
+      - name: Install nodejs
+        run: |
+          curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
+          export NVM_DIR="$HOME/.nvm"
+          [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
+          [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
+          nvm install 22
+          nvm use 22
+          node -v 
+          nvm current
+
+      - name: Benchmark dump load in binary format
         timeout-minutes: 60
         env:
           AWS_ACCESS_KEY_ID: ${{secrets.CI_COVERAGE_AWS_AK}}
@@ -89,9 +100,9 @@ jobs:
           META: ${{matrix.meta}}
           START_META: true
         run: |
-          sudo -E GOCOVERDIR=$(pwd)/cover .github/scripts/command/load_dump_bench.sh test_dump_load_in_binary
+          sudo -E GOCOVERDIR=$(pwd)/cover .github/scripts/command/dump_load_bench.sh test_dump_load_in_binary
       
-      - name: Test dump load fast
+      - name: Benchmark dump load fast
         timeout-minutes: 60
         env:
           AWS_ACCESS_KEY_ID: ${{secrets.CI_COVERAGE_AWS_AK}}
@@ -100,9 +111,9 @@ jobs:
           META: ${{matrix.meta}}
           START_META: false
         run: |
-          sudo -E GOCOVERDIR=$(pwd)/cover .github/scripts/command/load_dump_bench.sh test_dump_load_fast
+          sudo -E GOCOVERDIR=$(pwd)/cover .github/scripts/command/dump_load_bench.sh test_dump_load_fast
 
-      - name: Test dump load
+      - name: Benchmark dump load
         timeout-minutes: 60
         env:
           AWS_ACCESS_KEY_ID: ${{secrets.CI_COVERAGE_AWS_AK}}
@@ -111,20 +122,21 @@ jobs:
           META: ${{matrix.meta}}
           START_META: false
         run: |
-          sudo -E GOCOVERDIR=$(pwd)/cover .github/scripts/command/load_dump_bench.sh test_dump_load
+          sudo -E GOCOVERDIR=$(pwd)/cover .github/scripts/command/dump_load_bench.sh test_dump_load
   
       - name: log
         if: always()
         run: | 
           tail -500 /var/log/juicefs.log
           grep "<FATAL>:" /var/log/juicefs.log && exit 1 || true
-          
+
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
-          
+
       - name: Setup upterm session
         if: failure() && (github.event.inputs.debug == 'true' || github.run_attempt != 1)
         # if: failure()
@@ -133,7 +145,7 @@ jobs:
 
   success-all-test:
     runs-on: ubuntu-latest
-    needs: [load]
+    needs: [dump_load_bench]
     if: always()
     steps:
       - uses: technote-space/workflow-conclusion-action@v3
@@ -143,6 +155,22 @@ jobs:
         if: env.WORKFLOW_CONCLUSION == 'failure'
         run: exit 1
 
+      - name: mount coverage dir
+        timeout-minutes: 5
+        uses: ./.github/actions/mount-coverage-dir
+        with:
+          mount_point: cover
+          access_key: ${{ secrets.CI_COVERAGE_AWS_AK }}
+          secret_key: ${{ secrets.CI_COVERAGE_AWS_SK }}
+          token: ${{ secrets.CI_COVERAGE_AWS_TOKEN }}
+
+      - name: upload total coverage report
+        timeout-minutes: 30
+        continue-on-error: true
+        uses: ./.github/actions/upload-total-coverage
+        with:
+          UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
+
       - name: Send Slack Notification
         if: failure() && github.event_name != 'workflow_dispatch' 
         uses: juicedata/slack-notify-action@main
diff --git a/.github/workflows/fsrand.yml b/.github/workflows/fsrand.yml
index 8b4147a9..cf4d2a87 100644
--- a/.github/workflows/fsrand.yml
+++ b/.github/workflows/fsrand.yml
@@ -127,7 +127,7 @@ jobs:
       - name: check fsrand.log
         if: always()
         run: | 
-          tail -500 fsrand.log
+          [[ -f fsrand.log ]] && tail -500 fsrand.log || true
 
       - name: check juicefs.log
         if: always()
diff --git a/.github/workflows/ltpfs.yml b/.github/workflows/ltpfs.yml
index 2540aebb..b438ffc3 100644
--- a/.github/workflows/ltpfs.yml
+++ b/.github/workflows/ltpfs.yml
@@ -89,6 +89,7 @@ jobs:
           
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
diff --git a/.github/workflows/ltpsyscalls.yml b/.github/workflows/ltpsyscalls.yml
index 427f4dee..5af53176 100644
--- a/.github/workflows/ltpsyscalls.yml
+++ b/.github/workflows/ltpsyscalls.yml
@@ -134,6 +134,7 @@ jobs:
 
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
diff --git a/.github/workflows/pysdk.yml b/.github/workflows/pysdk.yml
index e7b4db7e..1dda4d7f 100644
--- a/.github/workflows/pysdk.yml
+++ b/.github/workflows/pysdk.yml
@@ -1,28 +1,28 @@
 name: "pysdk"
 
 on:
-  # push:
-  #   branches:
-  #   - main
-  #   - release**
-  #   paths:
-  #   - '**/hypo/fs_op.py'
-  #   - '**/hypo/fs.py'
-  #   - '**/hypo/fs_sdk_test.py'
-  #   - '**/test_pysdk.py'
-  #   - '**/juicefs.py'
-  #   - '**/pysdk.yml'
-  # pull_request:
-  #   branches:
-  #   - main
-  #   - release**
-  #   paths:
-  #   - '**/hypo/fs_op.py'
-  #   - '**/hypo/fs.py'
-  #   - '**/hypo/fs_sdk_test.py'
-  #   - '**/test_pysdk.py'
-  #   - '**/juicefs.py'
-  #   - '**/pysdk.yml'
+  push:
+    branches:
+    - main
+    - release**
+    paths:
+    - '**/hypo/fs_op.py'
+    - '**/hypo/fs.py'
+    - '**/hypo/fs_sdk_test.py'
+    - '**/test_pysdk.py'
+    - '**/juicefs.py'
+    - '**/pysdk.yml'
+  pull_request:
+    branches:
+    - main
+    - release**
+    paths:
+    - '**/hypo/fs_op.py'
+    - '**/hypo/fs.py'
+    - '**/hypo/fs_sdk_test.py'
+    - '**/test_pysdk.py'
+    - '**/juicefs.py'
+    - '**/pysdk.yml'
   schedule:
     - cron:  '0 19 * * *'
   workflow_dispatch:
@@ -160,6 +160,7 @@ jobs:
           sudo USE_SDK=true META_URL=$meta_url python3 .github/scripts/hypo/fs_sdk_test.py
 
       - name: Run fs.py
+        if: false
         run: |
           source .github/scripts/start_meta_engine.sh
           meta_url=$(get_meta_url ${{matrix.meta}})
@@ -174,6 +175,7 @@ jobs:
 
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
diff --git a/.github/workflows/rmfiles.yml b/.github/workflows/rmfiles.yml
index f288b05f..acc78eff 100644
--- a/.github/workflows/rmfiles.yml
+++ b/.github/workflows/rmfiles.yml
@@ -129,6 +129,7 @@ jobs:
 
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}          
diff --git a/.github/workflows/storage.yml b/.github/workflows/storage.yml
index a225878a..a1575862 100644
--- a/.github/workflows/storage.yml
+++ b/.github/workflows/storage.yml
@@ -78,6 +78,7 @@ jobs:
 
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}       
diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml
index 88a3ddb3..1ae7c7b1 100644
--- a/.github/workflows/sync.yml
+++ b/.github/workflows/sync.yml
@@ -118,7 +118,8 @@ jobs:
           token: ${{ secrets.CI_COVERAGE_AWS_TOKEN }}
 
       - name: upload total coverage report
-        timeout-minutes: 5
+        timeout-minutes: 30
+        continue-on-error: true
         uses: ./.github/actions/upload-total-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
diff --git a/.github/workflows/vdbench.yml b/.github/workflows/vdbench.yml
index 059ed505..45d2a732 100644
--- a/.github/workflows/vdbench.yml
+++ b/.github/workflows/vdbench.yml
@@ -120,6 +120,7 @@ jobs:
 
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }} 
diff --git a/.github/workflows/xattr.yml b/.github/workflows/xattr.yml
index 58ebbb23..1dcf9672 100644
--- a/.github/workflows/xattr.yml
+++ b/.github/workflows/xattr.yml
@@ -131,6 +131,7 @@ jobs:
 
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
diff --git a/cmd/flags.go b/cmd/flags.go
index 92555690..36068af6 100644
--- a/cmd/flags.go
+++ b/cmd/flags.go
@@ -129,7 +129,7 @@ func storageFlags() []cli.Flag {
 		},
 		&cli.IntFlag{
 			Name:  "max-stage-write",
-			Value: 0, // Enable this to have concurrent uploads to two backends, and get write bandwidth equals to sum of the two
+			Value: 1000, // large enough for normal cases, also prevents unlimited concurrency in abnormal cases
 			Usage: "number of threads allowed to write staged files, other requests will be uploaded directly (this option is only effective when 'writeback' mode is enabled)",
 		},
 		&cli.IntFlag{
diff --git a/cmd/gateway.go b/cmd/gateway.go
index 37b34e01..3b8ddcc3 100644
--- a/cmd/gateway.go
+++ b/cmd/gateway.go
@@ -80,6 +80,10 @@ func cmdGateway() *cli.Command {
 			Name:  "object-tag",
 			Usage: "enable object tagging api",
 		},
+		&cli.BoolFlag{
+			Name:  "object-meta",
+			Usage: "enable object metadata api",
+		},
 		&cli.StringFlag{
 			Name:  "domain",
 			Usage: "domain for virtual-host-style requests",
@@ -149,10 +153,11 @@ func gateway(c *cli.Context) error {
 		jfs,
 		conf,
 		&jfsgateway.Config{
-			MultiBucket: c.Bool("multi-buckets"),
-			KeepEtag:    c.Bool("keep-etag"),
-			Umask:       uint16(umask),
-			ObjTag:      c.Bool("object-tag"),
+			MultiBucket:   c.Bool("multi-buckets"),
+			KeepEtag:      c.Bool("keep-etag"),
+			Umask:         uint16(umask),
+			ObjTag:        c.Bool("object-tag"),
+			ObjMeta:       c.Bool("object-meta"),
 		},
 	)
 	if err != nil {
diff --git a/cmd/stats.go b/cmd/stats.go
index 779ba219..9eba3989 100644
--- a/cmd/stats.go
+++ b/cmd/stats.go
@@ -50,7 +50,7 @@ Details: https://juicefs.com/docs/community/fault_diagnosis_and_analysis#stats`,
 			&cli.StringFlag{
 				Name:  "schema",
 				Value: "ufmco",
-				Usage: "schema string that controls the output sections (u: usage, f: fuse, m: meta, c: blockcache, o: object, g: go)",
+				Usage: "schema string of output sections (t:time, u: usage, f: fuse, m: meta, c: blockcache, o: object, g: go)",
 			},
 			&cli.UintFlag{
 				Name:  "interval",
@@ -62,6 +62,11 @@ Details: https://juicefs.com/docs/community/fault_diagnosis_and_analysis#stats`,
 				Aliases: []string{"l"},
 				Usage:   "verbosity level, 0 or 1 is enough for most cases",
 			},
+			&cli.UintFlag{
+				Name:    "count",
+				Aliases: []string{"c"},
+				Usage:   "number of updates to display before exiting",
+			},
 		},
 	}
 }
@@ -84,6 +89,7 @@ const (
 	COLOR_DARK_SEQ = "\033[0;" // %dm
 	UNDERLINE_SEQ  = "\033[4m"
 	CLEAR_SCREEM   = "\033[2J\033[1;1H"
+	UNIXTIME_FMT   = "01-02 15:04:05"
 	// BOLD_SEQ       = "\033[1m"
 )
 
@@ -119,6 +125,7 @@ const (
 	metricGauge
 	metricCounter
 	metricHist
+	metricUnixtime
 )
 
 type item struct {
@@ -136,6 +143,9 @@ func (w *statsWatcher) buildSchema(schema string, verbosity uint) {
 	for _, r := range schema {
 		var s section
 		switch r {
+		case 't':
+			s.name = "system"
+			s.items = append(s.items, &item{"time", "juicefs_timestamp", metricUnixtime})
 		case 'u':
 			s.name = "usage"
 			s.items = append(s.items, &item{"cpu", "juicefs_cpu_usage", metricCPU | metricCounter})
@@ -210,7 +220,11 @@ func (w *statsWatcher) formatHeader() {
 	for i, s := range w.sections {
 		subs := make([]string, 0, len(s.items))
 		for _, it := range s.items {
-			subs = append(subs, w.colorize(padding(it.nick, 5, ' '), BLUE, false, true))
+			if (it.typ & 0xF0) == metricUnixtime {
+				subs = append(subs, w.colorize(padding(it.nick, len(UNIXTIME_FMT), ' '), BLUE, false, true))
+			} else {
+				subs = append(subs, w.colorize(padding(it.nick, 5, ' '), BLUE, false, true))
+			}
 			if it.typ&metricHist != 0 {
 				if it.typ&metricTime != 0 {
 					subs = append(subs, w.colorize(" lat ", BLUE, false, true))
@@ -220,6 +234,9 @@ func (w *statsWatcher) formatHeader() {
 			}
 		}
 		width := 6*len(subs) - 1 // nick(5) + space(1)
+		if s.name == "system" {
+			width = len(UNIXTIME_FMT)
+		}
 		subHeaders[i] = strings.Join(subs, " ")
 		headers[i] = w.colorize(padding(s.name, width, '-'), BLUE, true, false)
 	}
@@ -301,6 +318,12 @@ func (w *statsWatcher) printDiff(left, right map[string]float64, dark bool) {
 		vals := make([]string, 0, len(s.items))
 		for _, it := range s.items {
 			switch it.typ & 0xF0 {
+			case metricUnixtime: // current timestamp
+				if dark {
+					vals = append(vals, w.colorize(time.Now().Format(UNIXTIME_FMT), BLACK, false, false))
+				} else {
+					vals = append(vals, w.colorize(time.Now().Format(UNIXTIME_FMT), WHITE, true, false))
+				}
 			case metricGauge: // currently must be metricByte
 				vals = append(vals, w.formatU64(right[it.name], dark, true))
 			case metricCounter:
@@ -388,6 +411,7 @@ func stats(ctx *cli.Context) error {
 	}
 	watcher.buildSchema(ctx.String("schema"), ctx.Uint("verbosity"))
 	watcher.formatHeader()
+	count := ctx.Uint("count")
 
 	var tick uint
 	var start, last, current map[string]float64
@@ -406,9 +430,13 @@ func stats(ctx *cli.Context) error {
 		} else {
 			watcher.printDiff(last, current, true)
 		}
+		if count > 0 && tick >= watcher.interval*(count-1) {
+			break
+		}
 		last = current
 		tick++
 		<-ticker.C
 		current = readStats(watcher.mp)
 	}
+	return nil
 }
diff --git a/cmd/warmup.go b/cmd/warmup.go
index 56acf8e7..b59568ca 100644
--- a/cmd/warmup.go
+++ b/cmd/warmup.go
@@ -299,7 +299,7 @@ func warmup(ctx *cli.Context) error {
 		case vfs.EvictCache:
 			logger.Infof("%s: %d files (%s bytes)", action, count, humanize.IBytes(uint64(bytes)))
 		case vfs.CheckCache:
-			logger.Infof("%s: %d files (%s of %s (%2.1f%%)) cached", action, count,
+			logger.Infof("%s: %d files checked, %s of %s (%2.1f%%) cached", action, count,
 				humanize.IBytes(uint64(bytes)-total.MissBytes),
 				humanize.IBytes(uint64(bytes)),
 				float64(uint64(bytes)-total.MissBytes)*100/float64(bytes))
diff --git a/cmd/webdav.go b/cmd/webdav.go
index d76467e4..e0fd4ac8 100644
--- a/cmd/webdav.go
+++ b/cmd/webdav.go
@@ -64,6 +64,12 @@ func cmdWebDav() *cli.Command {
 			Aliases: []string{"d"},
 			Usage:   "run in background",
 		},
+                &cli.IntFlag{
+                        Name:    "threads",
+                        Aliases: []string{"p"},
+                        Value:   50,
+                        Usage:   "number of threads for delete jobs (max 255)",
+                },
 	}
 
 	return &cli.Command{
@@ -95,6 +101,7 @@ func webdav(c *cli.Context) error {
 		CertFile:        c.String("cert-file"),
 		KeyFile:         c.String("key-file"),
 		EnableProppatch: c.Bool("enable-proppatch"),
+		MaxDeletes:      c.Int("threads"),
 	})
 	return jfs.Meta().CloseSession()
 }
diff --git a/docs/en/guide/gateway.md b/docs/en/guide/gateway.md
index bb38bc29..260b6734 100644
--- a/docs/en/guide/gateway.md
+++ b/docs/en/guide/gateway.md
@@ -147,6 +147,10 @@ By default, JuiceFS S3 Gateway does not save or return object ETag information.
 
 Object tags are not supported by default, but you can use `--object-tag` to enable them.
 
+### Enable object metadata
+
+Object metadata is not supported by default, but you can use `--object-meta` to enable them.
+
 ### Enable virtual host-style requests
 
 By default, JuiceFS S3 Gateway supports path-style requests in the format of `http://mydomain.com/bucket/object`. The `MINIO_DOMAIN` environment variable is used to enable virtual host-style requests. If the request's `Host` header information matches `(.+).mydomain.com`, the matched pattern `$1` is used as the bucket, and the path is used as the object.
diff --git a/docs/en/reference/_common_options.mdx b/docs/en/reference/_common_options.mdx
index 73b56726..521f4797 100644
--- a/docs/en/reference/_common_options.mdx
+++ b/docs/en/reference/_common_options.mdx
@@ -6,7 +6,7 @@
 |`--backup-meta=3600`|interval (in seconds) to automatically backup metadata in the object storage (0 means disable backup) (default: "3600")|
 |`--backup-skip-trash` <VersionAdd>1.2</VersionAdd>|skip files and directories in trash when backup metadata.|
 |`--heartbeat=12`|interval (in seconds) to send heartbeat; it's recommended that all clients use the same heartbeat value (default: "12")|
-|`--read-only`|allow lookup/read operations only (default: false)|
+|`--read-only`|Read-only mode, i.e. allow only lookup/read operations. Note that this option implies `--no-bgjob`, so read-only clients do not execute background jobs.|
 |`--no-bgjob`|Disable background jobs, default to false, which means clients by default carry out background jobs, including:<br/><ul><li>Clean up expired files in Trash (look for `cleanupDeletedFiles`, `cleanupTrash` in [`pkg/meta/base.go`](https://github.com/juicedata/juicefs/blob/main/pkg/meta/base.go))</li><li>Delete slices that's not referenced (look for `cleanupSlices` in [`pkg/meta/base.go`](https://github.com/juicedata/juicefs/blob/main/pkg/meta/base.go))</li><li>Clean up stale client sessions (look for `CleanStaleSessions` in [`pkg/meta/base.go`](https://github.com/juicedata/juicefs/blob/main/pkg/meta/base.go))</li></ul>Note that compaction isn't affected by this option, it happens automatically with file reads and writes, client will check if compaction is in need, and run in background (take Redis for example, look for `compactChunk` in [`pkg/meta/base.go`](https://github.com/juicedata/juicefs/blob/main/pkg/meta/redis.go)).|
 |`--atime-mode=noatime` <VersionAdd>1.1</VersionAdd> |Control atime (last time the file was accessed) behavior, support the following modes:<br/><ul><li>`noatime` (default): set when the file is created or when `SetAttr` is explicitly called. Accessing and modifying the file will not affect atime, tracking atime comes at a performance cost, so this is the default behavior</li><li>`relatime`: update inode access times relative to mtime (last time when the file data was modified) or ctime (last time when file metadata was changed). Only update atime if atime was earlier than the current mtime or ctime, or the file's atime is more than 1 day old</li><li>`strictatime`: always update atime on access</li></ul>|
 |`--skip-dir-nlink=20` <VersionAdd>1.1</VersionAdd> |number of retries after which the update of directory nlink will be skipped (used for tkv only, 0 means never) (default: 20)|
diff --git a/docs/zh_cn/reference/_common_options.mdx b/docs/zh_cn/reference/_common_options.mdx
index 5f3132d5..c88ecd4d 100644
--- a/docs/zh_cn/reference/_common_options.mdx
+++ b/docs/zh_cn/reference/_common_options.mdx
@@ -6,7 +6,7 @@
 |`--backup-meta=3600`|自动备份元数据到对象存储的间隔时间；单位秒，默认 3600，设为 0 表示不备份。|
 |`--backup-skip-trash` <VersionAdd>1.2</VersionAdd>|备份元数据时跳过回收站中的文件和目录。|
 |`--heartbeat=12`|发送心跳的间隔（单位秒），建议所有客户端使用相同的心跳值 (默认：12)|
-|`--read-only`|启用只读模式挂载。|
+|`--read-only`|只读模式，只允许 lookup 和 read 请求。注意，只读模式隐含了 `--no-bgjob`，因此只读客户端不会运行后台任务。|
 |`--no-bgjob`|禁用后台任务，默认为 false，也就是说客户端会默认运行后台任务。后台任务包含：<br/><ul><li>清理回收站中过期的文件（在 [`pkg/meta/base.go`](https://github.com/juicedata/juicefs/blob/main/pkg/meta/base.go) 中搜索 `cleanupDeletedFiles` 和 `cleanupTrash`）</li><li>清理引用计数为 0 的 Slice（在 [`pkg/meta/base.go`](https://github.com/juicedata/juicefs/blob/main/pkg/meta/base.go) 中搜索 `cleanupSlices`）</li><li>清理过期的客户端会话（在 [`pkg/meta/base.go`](https://github.com/juicedata/juicefs/blob/main/pkg/meta/base.go) 中搜索 `CleanStaleSessions`）</li></ul>特别地，与[企业版](https://juicefs.com/docs/zh/cloud/guide/background-job)不同，社区版碎片合并（Compaction）不受该选项的影响，而是随着文件读写操作，自动判断是否需要合并，然后异步执行（以 Redis 为例，在 [`pkg/meta/base.go`](https://github.com/juicedata/juicefs/blob/main/pkg/meta/redis.go) 中搜索 `compactChunk`）|
 |`--atime-mode=noatime` <VersionAdd>1.1</VersionAdd>|控制如何更新 atime（文件最后被访问的时间）。支持以下模式：<br/><ul><li>`noatime`（默认）：仅在文件创建和主动调用 `SetAttr` 时设置，平时访问与修改文件不影响 atime 值。考虑到更新 atime 需要运行额外的事务，对性能有影响，因此默认关闭。</li><li>`relatime`：仅在 mtime（文件内容修改时间）或 ctime（文件元数据修改时间）比 atime 新，或者 atime 超过 24 小时没有更新时进行更新。</li><li>`strictatime`：持续更新 atime</li></ul>|
 |`--skip-dir-nlink=20` <VersionAdd>1.1</VersionAdd>|跳过更新目录 nlink 前的重试次数 (仅用于 TKV, 0 代表永不跳过) (默认：20)|
diff --git a/go.mod b/go.mod
index 98b0f49c..da0f3cdb 100644
--- a/go.mod
+++ b/go.mod
@@ -41,7 +41,7 @@ require (
 	github.com/ks3sdklib/aws-sdk-go v1.2.2
 	github.com/l0wl3vel/bunny-storage-go-sdk v0.0.10
 	github.com/mattn/go-isatty v0.0.18
-	github.com/mattn/go-sqlite3 v1.14.16
+	github.com/mattn/go-sqlite3 v1.14.24
 	github.com/minio/cli v1.24.2
 	github.com/minio/minio v0.0.0-20210206053228-97fe57bba92c
 	github.com/minio/minio-go/v7 v7.0.11-0.20210302210017-6ae69c73ce78
diff --git a/go.sum b/go.sum
index 2e2d5224..ce367625 100644
--- a/go.sum
+++ b/go.sum
@@ -596,8 +596,8 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
-github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
-github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
diff --git a/pkg/chunk/cached_store.go b/pkg/chunk/cached_store.go
index 993cdfe5..4fc718fd 100644
--- a/pkg/chunk/cached_store.go
+++ b/pkg/chunk/cached_store.go
@@ -448,11 +448,18 @@ func (s *wSlice) upload(indx int) {
 		}
 		if s.store.conf.Writeback {
 			stagingPath := "unknown"
+			stageFailed := false
+			block.Acquire()
 			err := utils.WithTimeout(func() (err error) { // In case it hangs for more than 5 minutes(see fileWriter.flush), fallback to uploading directly to avoid `EIO`
+				defer block.Release()
 				stagingPath, err = s.store.bcache.stage(key, block.Data, s.store.shouldCache(blen))
+				if err == nil && stageFailed { // upload thread already marked me as failed because of timeout
+					_ = s.store.bcache.removeStage(key)
+				}
 				return err
 			}, s.store.conf.PutTimeout)
 			if err != nil {
+				stageFailed = true
 				if !errors.Is(err, errStageConcurrency) {
 					s.store.stageBlockErrors.Add(1)
 					logger.Warnf("write %s to disk: %s, upload it directly", stagingPath, err)
@@ -590,10 +597,6 @@ func (c *Config) SelfCheck(uuid string) {
 			c.UploadDelay = 0
 			c.UploadHours = ""
 		}
-		if c.MaxStageWrite > 0 {
-			logger.Warnf("max-stage-write is disabled in non-writeback mode")
-			c.MaxStageWrite = 0
-		}
 	}
 	if _, _, err := c.parseHours(); err != nil {
 		logger.Warnf("invalid value (%s) for upload-hours: %s", c.UploadHours, err)
@@ -617,6 +620,9 @@ func (c *Config) SelfCheck(uuid string) {
 			logger.Warnf("verify-cache-checksum should be one of %v", cs)
 			c.CacheChecksum = CsFull
 		}
+	} else if c.Writeback {
+		logger.Warnf("writeback is not supported in memory cache mode")
+		c.Writeback = false
 	}
 	if c.CacheEviction == "" {
 		c.CacheEviction = "2-random"
diff --git a/pkg/chunk/disk_cache.go b/pkg/chunk/disk_cache.go
index 10f5f845..9f4f37b0 100644
--- a/pkg/chunk/disk_cache.go
+++ b/pkg/chunk/disk_cache.go
@@ -1041,7 +1041,6 @@ type CacheManager interface {
 	uploaded(key string, size int)
 	stage(key string, data []byte, keepCache bool) (string, error)
 	removeStage(key string) error
-	stagePath(key string) string
 	stats() (int64, int64)
 	usedMemory() int64
 	isEmpty() bool
@@ -1248,14 +1247,6 @@ func (m *cacheManager) stage(key string, data []byte, keepCache bool) (string, e
 	return "", errors.New("no available cache dir")
 }
 
-func (m *cacheManager) stagePath(key string) string {
-	store := m.getStore(key)
-	if store != nil {
-		return store.stagePath(key)
-	}
-	return ""
-}
-
 func (m *cacheManager) uploaded(key string, size int) {
 	store := m.getStore(key)
 	if store != nil {
diff --git a/pkg/chunk/mem_cache.go b/pkg/chunk/mem_cache.go
index eebcbf64..03ca79cf 100644
--- a/pkg/chunk/mem_cache.go
+++ b/pkg/chunk/mem_cache.go
@@ -210,6 +210,5 @@ func (c *memcache) stage(key string, data []byte, keepCache bool) (string, error
 	return "", errors.New("not supported")
 }
 func (c *memcache) uploaded(key string, size int)    {}
-func (c *memcache) stagePath(key string) string      { return "" }
 func (c *memcache) isEmpty() bool                    { return false }
 func (c *memcache) getMetrics() *cacheManagerMetrics { return c.metrics }
diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go
index 0a0573ef..c800a595 100644
--- a/pkg/fs/fs.go
+++ b/pkg/fs/fs.go
@@ -209,6 +209,7 @@ func (fs *FileSystem) InitMetrics(reg prometheus.Registerer) {
 		reg.MustRegister(fs.readSizeHistogram)
 		reg.MustRegister(fs.writtenSizeHistogram)
 		reg.MustRegister(fs.opsDurationsHistogram)
+		vfs.InitMemoryBufferMetrics(fs.writer, fs.reader, reg)
 	}
 }
 
@@ -462,12 +463,18 @@ func (fs *FileSystem) Mkdir(ctx meta.Context, p string, mode uint16, umask uint1
 }
 
 func (fs *FileSystem) MkdirAll(ctx meta.Context, p string, mode uint16, umask uint16) (err syscall.Errno) {
+	return fs.MkdirAll0(ctx, p, mode, umask, true)
+}
+
+func (fs *FileSystem) MkdirAll0(ctx meta.Context, p string, mode uint16, umask uint16, existOK bool) (err syscall.Errno) {
 	err = fs.Mkdir(ctx, p, mode, umask)
 	if err == syscall.ENOENT {
-		_ = fs.MkdirAll(ctx, parentDir(p), mode, umask)
-		err = fs.Mkdir(ctx, p, mode, umask)
+		err = fs.MkdirAll(ctx, parentDir(p), mode, umask)
+		if err == 0 {
+			err = fs.Mkdir(ctx, p, mode, umask)
+		}
 	}
-	if err == syscall.EEXIST {
+	if existOK && err == syscall.EEXIST {
 		err = 0
 	}
 	return err
@@ -494,7 +501,7 @@ func (fs *FileSystem) Delete(ctx meta.Context, p string) (err syscall.Errno) {
 	return
 }
 
-func (fs *FileSystem) Rmr(ctx meta.Context, p string) (err syscall.Errno) {
+func (fs *FileSystem) Rmr(ctx meta.Context, p string, numthreads int) (err syscall.Errno) {
 	defer trace.StartRegion(context.TODO(), "fs.Rmr").End()
 	l := vfs.NewLogContext(ctx)
 	defer func() { fs.log(l, "Rmr (%s): %s", p, errstr(err)) }()
@@ -502,7 +509,7 @@ func (fs *FileSystem) Rmr(ctx meta.Context, p string) (err syscall.Errno) {
 	if err != 0 {
 		return
 	}
-	err = fs.m.Remove(ctx, parent.inode, path.Base(p), false, meta.RmrDefaultThreads, nil)
+	err = fs.m.Remove(ctx, parent.inode, path.Base(p), false, numthreads, nil)
 	fs.invalidateEntry(parent.inode, path.Base(p))
 	return
 }
diff --git a/pkg/fs/http.go b/pkg/fs/http.go
index b327b395..69cacea8 100644
--- a/pkg/fs/http.go
+++ b/pkg/fs/http.go
@@ -125,7 +125,7 @@ func (hfs *webdavFS) OpenFile(ctx context.Context, name string, flag int, perm o
 }
 
 func (hfs *webdavFS) RemoveAll(ctx context.Context, name string) error {
-	return econv(hfs.fs.Rmr(hfs.ctx, name))
+	return econv(hfs.fs.Rmr(hfs.ctx, name, hfs.config.MaxDeletes))
 }
 
 func (hfs *webdavFS) Rename(ctx context.Context, oldName, newName string) error {
@@ -252,6 +252,7 @@ type WebdavConfig struct {
 	Password        string
 	CertFile        string
 	KeyFile         string
+	MaxDeletes	int
 }
 
 type indexHandler struct {
diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go
index 47af9b36..08466559 100644
--- a/pkg/fuse/fuse.go
+++ b/pkg/fuse/fuse.go
@@ -19,6 +19,7 @@ package fuse
 import (
 	"fmt"
 	"log"
+	"math"
 	"os"
 	"os/exec"
 	"runtime"
@@ -298,7 +299,12 @@ func (fs *fileSystem) Fallocate(cancel <-chan struct{}, in *fuse.FallocateIn) (c
 func (fs *fileSystem) CopyFileRange(cancel <-chan struct{}, in *fuse.CopyFileRangeIn) (written uint32, code fuse.Status) {
 	ctx := fs.newContext(cancel, &in.InHeader)
 	defer releaseContext(ctx)
-	copied, err := fs.v.CopyFileRange(ctx, Ino(in.NodeId), in.FhIn, in.OffIn, Ino(in.NodeIdOut), in.FhOut, in.OffOut, in.Len, uint32(in.Flags))
+	var len = in.Len
+	if len > math.MaxUint32 {
+		// written may overflow
+		len = math.MaxUint32 + 1 - meta.ChunkSize
+	}
+	copied, err := fs.v.CopyFileRange(ctx, Ino(in.NodeId), in.FhIn, in.OffIn, Ino(in.NodeIdOut), in.FhOut, in.OffOut, len, uint32(in.Flags))
 	if err != 0 {
 		return 0, fuse.Status(err)
 	}
diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go
index f5363ba9..0808efb4 100644
--- a/pkg/gateway/gateway.go
+++ b/pkg/gateway/gateway.go
@@ -18,6 +18,7 @@ package gateway
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -48,8 +49,9 @@ import (
 )
 
 const (
-	sep        = "/"
-	metaBucket = ".sys"
+	sep          = "/"
+	metaBucket   = ".sys"
+	subDirPrefix = 3 // 16^3=4096 slots
 )
 
 var mctx meta.Context
@@ -60,6 +62,7 @@ type Config struct {
 	KeepEtag    bool
 	Umask       uint16
 	ObjTag      bool
+	ObjMeta     bool
 }
 
 func NewJFSGateway(jfs *fs.FileSystem, conf *vfs.Config, gConf *Config) (minio.ObjectLayer, error) {
@@ -186,10 +189,14 @@ func (n *jfsObjects) tpath(p ...string) string {
 }
 
 func (n *jfsObjects) upath(bucket, uploadID string) string {
-	return n.tpath(bucket, "uploads", uploadID)
+	return n.tpath(bucket, "uploads", uploadID[:subDirPrefix], uploadID)
 }
 
 func (n *jfsObjects) ppath(bucket, uploadID, part string) string {
+	return n.tpath(bucket, "uploads", uploadID[:subDirPrefix], uploadID, part)
+}
+
+func (n *jfsObjects) ppathFlat(bucket, uploadID, part string) string { // compatible with tmp files uploaded by old versions(<1.2)
 	return n.tpath(bucket, "uploads", uploadID, part)
 }
 
@@ -356,8 +363,11 @@ func (n *jfsObjects) checkBucket(ctx context.Context, bucket string) error {
 	if err := n.isValidBucketName(bucket); err != nil {
 		return err
 	}
-	if _, eno := n.fs.Stat(mctx, n.path(bucket)); eno != 0 {
-		return jfsToObjectErr(ctx, eno, bucket)
+	bucketPath := n.path(bucket)
+	if bucketPath != "/" { // no need to stat "/" in every request
+		if _, eno := n.fs.Stat(mctx, bucketPath); eno != 0 {
+			return jfsToObjectErr(ctx, eno, bucket)
+		}
 	}
 	return nil
 }
@@ -533,10 +543,17 @@ func (n *jfsObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
 	}
 	dst := n.path(dstBucket, dstObject)
 	src := n.path(srcBucket, srcObject)
+
 	if minio.IsStringEqual(src, dst) {
+		// if we copy the same object for set metadata
+		err = n.setObjMeta(dst, srcInfo.UserDefined)
+		if err != nil {
+			logger.Errorf("set object metadata error, path: %s error %s", dst, err)
+		}
 		return n.GetObjectInfo(ctx, srcBucket, srcObject, minio.ObjectOptions{})
 	}
-	tmp := n.tpath(dstBucket, "tmp", minio.MustGetUUID())
+	uuid := minio.MustGetUUID()
+	tmp := n.tpath(dstBucket, "tmp", uuid[:subDirPrefix], uuid)
 	f, eno := n.fs.Create(mctx, tmp, 0666, n.gConf.Umask)
 	if eno == syscall.ENOENT {
 		_ = n.mkdirAll(ctx, path.Dir(tmp))
@@ -548,7 +565,9 @@ func (n *jfsObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
 	}
 	defer func() {
 		_ = f.Close(mctx)
-		_ = n.fs.Delete(mctx, tmp)
+		if err != nil {
+			_ = n.fs.Delete(mctx, tmp)
+		}
 	}()
 
 	_, eno = n.fs.CopyFileRange(mctx, src, 0, tmp, 0, 1<<63)
@@ -577,6 +596,10 @@ func (n *jfsObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
 			}
 		}
 	}
+	err = n.setObjMeta(tmp, srcInfo.UserDefined)
+	if err != nil {
+		logger.Errorf("set object metadata error, path: %s error %s", dst, err)
+	}
 
 	eno = n.fs.Rename(mctx, tmp, dst, 0)
 	if eno == syscall.ENOENT {
@@ -687,6 +710,20 @@ func (n *jfsObjects) GetObjectInfo(ctx context.Context, bucket, object string, o
 			return minio.ObjectInfo{}, errno
 		}
 	}
+	objMeta, err := n.getObjMeta(n.path(bucket, object))
+	if err != nil {
+		return minio.ObjectInfo{}, err
+	}
+	if opts.UserDefined == nil {
+		opts.UserDefined = make(map[string]string)
+	}
+	for k, v := range objMeta {
+		opts.UserDefined[k] = v
+	}
+	contentType := utils.GuessMimeType(object)
+	if c, exist := objMeta["content-type"]; exist && len(c) > 0 {
+		contentType = c
+	}
 	return minio.ObjectInfo{
 		Bucket:      bucket,
 		Name:        object,
@@ -695,7 +732,7 @@ func (n *jfsObjects) GetObjectInfo(ctx context.Context, bucket, object string, o
 		IsDir:       fi.IsDir(),
 		AccTime:     fi.ModTime(),
 		ETag:        string(etag),
-		ContentType: utils.GuessMimeType(object),
+		ContentType: contentType,
 		UserTags:    string(tagStr),
 		UserDefined: minio.CleanMetadata(opts.UserDefined),
 	}, nil
@@ -725,7 +762,8 @@ func (n *jfsObjects) mkdirAll(ctx context.Context, p string) error {
 }
 
 func (n *jfsObjects) putObject(ctx context.Context, bucket, object string, r *minio.PutObjReader, opts minio.ObjectOptions, applyObjTaggingFunc func(tmpName string)) (err error) {
-	tmpname := n.tpath(bucket, "tmp", minio.MustGetUUID())
+	uuid := minio.MustGetUUID()
+	tmpname := n.tpath(bucket, "tmp", uuid[:subDirPrefix], uuid)
 	f, eno := n.fs.Create(mctx, tmpname, 0666, n.gConf.Umask)
 	if eno == syscall.ENOENT {
 		_ = n.mkdirAll(ctx, path.Dir(tmpname))
@@ -736,7 +774,11 @@ func (n *jfsObjects) putObject(ctx context.Context, bucket, object string, r *mi
 		err = eno
 		return
 	}
-	defer func() { _ = n.fs.Delete(mctx, tmpname) }()
+	defer func() {
+		if err != nil {
+			_ = n.fs.Delete(mctx, tmpname)
+		}
+	}()
 	var buf = buffPool.Get().(*[]byte)
 	defer buffPool.Put(buf)
 	for {
@@ -821,6 +863,10 @@ func (n *jfsObjects) PutObject(ctx context.Context, bucket string, object string
 					}
 				}
 			}
+			err = n.setObjMeta(tmpName, opts.UserDefined)
+			if err != nil {
+				logger.Errorf("set object metadata error, path: %s error %s", p, err)
+			}
 		}); err != nil {
 			return
 		}
@@ -829,6 +875,7 @@ func (n *jfsObjects) PutObject(ctx context.Context, bucket string, object string
 	if eno != 0 {
 		return objInfo, jfsToObjectErr(ctx, eno, bucket, object)
 	}
+
 	return minio.ObjectInfo{
 		Bucket:      bucket,
 		Name:        object,
@@ -861,6 +908,10 @@ func (n *jfsObjects) NewMultipartUpload(ctx context.Context, bucket string, obje
 				}
 			}
 		}
+		err = n.setObjMeta(p, opts.UserDefined)
+		if err != nil {
+			logger.Errorf("set object metadata error, path: %s  error %s", p, err)
+		}
 	}
 	return
 }
@@ -871,6 +922,62 @@ const s3Etag = "s3-etag"
 // less than 64k ref: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#tag-restrictions
 const s3Tags = "s3-tags"
 
+// S3 object metadata
+const s3Meta = "s3-meta"
+const amzMeta = "x-amz-meta-"
+
+var s3UserControlledSystemMeta = []string{
+	"cache-control",
+	"content-disposition",
+	"content-type",
+}
+
+func (n *jfsObjects) getObjMeta(p string) (objMeta map[string]string, err error) {
+	if n.gConf.ObjMeta {
+		var errno syscall.Errno
+		var metadataStr []byte
+		if metadataStr, errno = n.fs.GetXattr(mctx, p, s3Meta); errno != 0 && errno != meta.ENOATTR {
+			return objMeta, errno
+		}
+		if len(metadataStr) > 0 {
+			err = json.Unmarshal(metadataStr, &objMeta)
+			return objMeta, err
+		}
+	} else {
+		objMeta = make(map[string]string)
+	}
+	return objMeta, nil
+}
+
+func (n *jfsObjects) setObjMeta(p string, metadata map[string]string) error {
+	if n.gConf.ObjMeta && metadata != nil {
+		meta := make(map[string]string)
+		for k, v := range metadata {
+			k = strings.ToLower(k)
+			if strings.HasPrefix(k, amzMeta) {
+				meta[k] = v
+			} else {
+				for _, systemMetaKey := range s3UserControlledSystemMeta {
+					if k == systemMetaKey {
+						meta[k] = v
+						break
+					}
+				}
+			}
+		}
+		if len(meta) > 0 {
+			s3MetadataValue, err := json.Marshal(meta)
+			if err != nil {
+				return err
+			}
+			if eno := n.fs.SetXattr(mctx, p, s3Meta, s3MetadataValue, 0); eno != 0 {
+				logger.Errorf("set object metadata error, path: %s,value: %s error: %s", p, string(s3Meta), eno)
+			}
+		}
+	}
+	return nil
+}
+
 func (n *jfsObjects) ListMultipartUploads(ctx context.Context, bucket string, prefix string, keyMarker string, uploadIDMarker string, delimiter string, maxUploads int) (lmi minio.ListMultipartsInfo, err error) {
 	if err = n.checkBucket(ctx, bucket); err != nil {
 		return
@@ -880,7 +987,7 @@ func (n *jfsObjects) ListMultipartUploads(ctx context.Context, bucket string, pr
 		return // no found
 	}
 	defer f.Close(mctx)
-	entries, eno := f.ReaddirPlus(mctx, 0)
+	parents, eno := f.ReaddirPlus(mctx, 0)
 	if eno != 0 {
 		err = jfsToObjectErr(ctx, eno, bucket)
 		return
@@ -891,22 +998,38 @@ func (n *jfsObjects) ListMultipartUploads(ctx context.Context, bucket string, pr
 	lmi.MaxUploads = maxUploads
 	lmi.Delimiter = delimiter
 	commPrefixSet := make(map[string]struct{})
-	for _, e := range entries {
-		uploadID := string(e.Name)
-		// todo: parallel
-		object_, eno := n.fs.GetXattr(mctx, n.upath(bucket, uploadID), uploadKeyName)
+	for _, p := range parents {
+		f, eno := n.fs.Open(mctx, n.tpath(bucket, "uploads", string(p.Name)), 0)
 		if eno != 0 {
-			logger.Warnf("get object xattr error %s: %s, ignore this item", n.upath(bucket, uploadID), eno)
-			continue
+			return
 		}
-		object := string(object_)
-		if strings.HasPrefix(object, prefix) {
-			if keyMarker != "" && object+uploadID > keyMarker+uploadIDMarker || keyMarker == "" {
-				lmi.Uploads = append(lmi.Uploads, minio.MultipartInfo{
-					Object:    object,
-					UploadID:  uploadID,
-					Initiated: time.Unix(e.Attr.Atime, int64(e.Attr.Atimensec)),
-				})
+		defer f.Close(mctx)
+		entries, eno := f.ReaddirPlus(mctx, 0)
+		if eno != 0 {
+			err = jfsToObjectErr(ctx, eno, bucket)
+			return
+		}
+
+		for _, e := range entries {
+			if len(e.Name) != 36 {
+				continue // not an uuid
+			}
+			uploadID := string(e.Name)
+			// todo: parallel
+			object_, eno := n.fs.GetXattr(mctx, n.upath(bucket, uploadID), uploadKeyName)
+			if eno != 0 {
+				logger.Warnf("get object xattr error %s: %s, ignore this item", n.upath(bucket, uploadID), eno)
+				continue
+			}
+			object := string(object_)
+			if strings.HasPrefix(object, prefix) {
+				if keyMarker != "" && object+uploadID > keyMarker+uploadIDMarker || keyMarker == "" {
+					lmi.Uploads = append(lmi.Uploads, minio.MultipartInfo{
+						Object:    object,
+						UploadID:  uploadID,
+						Initiated: time.Unix(e.Attr.Atime, int64(e.Attr.Atimensec)),
+					})
+				}
 			}
 		}
 	}
@@ -1071,6 +1194,10 @@ func (n *jfsObjects) CompleteMultipartUpload(ctx context.Context, bucket, object
 	for _, part := range parts {
 		p := n.ppath(bucket, uploadID, strconv.Itoa(part.PartNumber))
 		copied, eno := n.fs.CopyFileRange(mctx, p, 0, tmp, total, 5<<30)
+		if eno == syscall.ENOENT { // try lookup from old path
+			p = n.ppathFlat(bucket, uploadID, strconv.Itoa(part.PartNumber))
+			copied, eno = n.fs.CopyFileRange(mctx, p, 0, tmp, total, 5<<30)
+		}
 		if eno != 0 {
 			err = jfsToObjectErr(ctx, eno, bucket, object, uploadID)
 			logger.Errorf("merge parts: %s", err)
@@ -1100,6 +1227,15 @@ func (n *jfsObjects) CompleteMultipartUpload(ctx context.Context, bucket, object
 		}
 	}
 
+	var objMeta map[string]string
+	if n.gConf.ObjMeta {
+		if objMeta, err = n.getObjMeta(n.upath(bucket, uploadID)); err != nil {
+			logger.Errorf("get object meta error, path: %s, error: %s", n.upath(bucket, uploadID), err)
+		} else if err = n.setObjMeta(tmp, objMeta); err != nil {
+			logger.Errorf("set object meta error, path: %s, error: %s", tmp, err)
+		}
+	}
+
 	name := n.path(bucket, object)
 	eno = n.fs.Rename(mctx, tmp, name, 0)
 	if eno == syscall.ENOENT {
@@ -1126,16 +1262,17 @@ func (n *jfsObjects) CompleteMultipartUpload(ctx context.Context, bucket, object
 	}
 
 	// remove parts
-	_ = n.fs.Rmr(mctx, n.upath(bucket, uploadID))
+	_ = n.fs.Rmr(mctx, n.upath(bucket, uploadID), meta.RmrDefaultThreads)
 	return minio.ObjectInfo{
-		Bucket:   bucket,
-		Name:     object,
-		ETag:     s3MD5,
-		ModTime:  fi.ModTime(),
-		Size:     fi.Size(),
-		IsDir:    fi.IsDir(),
-		AccTime:  fi.ModTime(),
-		UserTags: string(tagStr),
+		Bucket:      bucket,
+		Name:        object,
+		ETag:        s3MD5,
+		ModTime:     fi.ModTime(),
+		Size:        fi.Size(),
+		IsDir:       fi.IsDir(),
+		AccTime:     fi.ModTime(),
+		UserTags:    string(tagStr),
+		UserDefined: minio.CleanMetadata(opts.UserDefined),
 	}, nil
 }
 
@@ -1143,12 +1280,12 @@ func (n *jfsObjects) AbortMultipartUpload(ctx context.Context, bucket, object, u
 	if err = n.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
 		return
 	}
-	eno := n.fs.Rmr(mctx, n.upath(bucket, uploadID))
+	eno := n.fs.Rmr(mctx, n.upath(bucket, uploadID), meta.RmrDefaultThreads)
 	return jfsToObjectErr(ctx, eno, bucket, object, uploadID)
 }
 
 func (n *jfsObjects) cleanup() {
-	for t := range time.Tick(24 * time.Hour) {
+	for range time.Tick(24 * time.Hour) {
 		// default bucket tmp dirs
 		tmpDirs := []string{".sys/tmp/", ".sys/uploads/"}
 		if n.gConf.MultiBucket {
@@ -1163,27 +1300,40 @@ func (n *jfsObjects) cleanup() {
 			}
 		}
 		for _, dir := range tmpDirs {
-			f, errno := n.fs.Open(mctx, dir, 0)
-			if errno != 0 {
+			n.cleanupDir(dir)
+		}
+	}
+}
+
+func (n *jfsObjects) cleanupDir(dir string) bool {
+	f, errno := n.fs.Open(mctx, dir, 0)
+	if errno != 0 {
+		return false
+	}
+	defer f.Close(mctx)
+	entries, _ := f.ReaddirPlus(mctx, 0)
+	now := time.Now()
+	deleted := 0
+	for _, entry := range entries {
+		dirPath := n.path(dir, string(entry.Name))
+		if entry.Attr.Typ == meta.TypeDirectory && len(entry.Name) == subDirPrefix {
+			if !n.cleanupDir(strings.TrimPrefix(dirPath, "/")) {
 				continue
 			}
-			entries, _ := f.ReaddirPlus(mctx, 0)
-			for _, entry := range entries {
-				if _, err := uuid.Parse(string(entry.Name)); err != nil {
-					continue
-				}
-				if t.Sub(time.Unix(entry.Attr.Mtime, 0)) > 7*24*time.Hour {
-					p := n.path(dir, string(entry.Name))
-					if errno := n.fs.Rmr(mctx, p); errno != 0 {
-						logger.Errorf("failed to delete expired temporary files path: %s,", p)
-					} else {
-						logger.Infof("delete expired temporary files path: %s, mtime: %s", p, time.Unix(entry.Attr.Mtime, 0).Format(time.RFC3339))
-					}
-				}
+		} else if _, err := uuid.Parse(string(entry.Name)); err != nil {
+			logger.Warnf("unexpected file path: %s", dirPath)
+			continue
+		}
+		if now.Sub(time.Unix(entry.Attr.Mtime, 0)) > 7*24*time.Hour {
+			if errno = n.fs.Rmr(mctx, dirPath, meta.RmrDefaultThreads); errno != 0 {
+				logger.Errorf("failed to delete expired temporary files path: %s, err: %s", dirPath, errno)
+			} else {
+				deleted += 1
+				logger.Infof("delete expired temporary files path: %s, mtime: %s", dirPath, time.Unix(entry.Attr.Mtime, 0).Format(time.RFC3339))
 			}
-			_ = f.Close(mctx)
 		}
 	}
+	return deleted == len(entries)
 }
 
 type jfsFLock struct {
diff --git a/pkg/meta/base.go b/pkg/meta/base.go
index 1d944ce8..3b262d60 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -1096,15 +1096,12 @@ func (m *baseMeta) GetAttr(ctx Context, inode Ino, attr *Attr) syscall.Errno {
 func (m *baseMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint8, attr *Attr) syscall.Errno {
 	defer m.timeit("SetAttr", time.Now())
 	inode = m.checkRoot(inode)
-	defer func() {
+	err := m.en.doSetAttr(ctx, inode, set, sugidclearmode, attr)
+	if err == 0 {
 		m.of.InvalidateChunk(inode, invalidateAttrOnly)
-		if set&(SetAttrAtime|SetAttrAtimeNow) != 0 {
-			if f := m.of.find(inode); f != nil {
-				f.attr.Full = false
-			}
-		}
-	}()
-	return m.en.doSetAttr(ctx, inode, set, sugidclearmode, attr)
+		m.of.Update(inode, attr)
+	}
+	return err
 }
 
 func (m *baseMeta) nextInode() (Ino, error) {
@@ -1177,6 +1174,9 @@ func (m *baseMeta) Mknod(ctx Context, parent Ino, name string, _type uint8, mode
 	if name == "" {
 		return syscall.ENOENT
 	}
+	if name == "." || name == ".." {
+		return syscall.EEXIST
+	}
 
 	defer m.timeit("Mknod", time.Now())
 	parent = m.checkRoot(parent)
diff --git a/pkg/meta/openfile.go b/pkg/meta/openfile.go
index 135d8a02..e370e6a2 100644
--- a/pkg/meta/openfile.go
+++ b/pkg/meta/openfile.go
@@ -167,7 +167,7 @@ func (o *openfiles) Check(ino Ino, attr *Attr) bool {
 
 func (o *openfiles) Update(ino Ino, attr *Attr) bool {
 	if attr == nil {
-		panic("attr is nil")
+		return false
 	}
 	o.Lock()
 	defer o.Unlock()
diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go
index 040a30e6..3563ceae 100644
--- a/pkg/meta/redis.go
+++ b/pkg/meta/redis.go
@@ -871,6 +871,7 @@ func (m *redisMeta) doLookup(ctx Context, parent Ino, name string, inode *Ino, a
 
 	if err == nil {
 		m.parseAttr(encodedAttr, attr)
+		m.of.Update(foundIno, attr)
 	} else if err == redis.Nil { // corrupt entry
 		logger.Warnf("no attribute for inode %d (%d, %s)", foundIno, parent, name)
 		*attr = Attr{Typ: foundType}
diff --git a/pkg/meta/sql.go b/pkg/meta/sql.go
index c25cc5e2..9f46bf2f 100644
--- a/pkg/meta/sql.go
+++ b/pkg/meta/sql.go
@@ -290,8 +290,8 @@ func retriveUrlConnsOptions(murl string) (string, int, int, int, int) {
 
 	var vOpenConns int = 0
 	var vIdleConns int = runtime.GOMAXPROCS(-1) * 2
-	var vIdleTime  int = 300
-	var vLifeTime  int = 0
+	var vIdleTime int = 300
+	var vLifeTime int = 0
 
 	if optIndex != -1 {
 		baseurl := murl[:optIndex]
@@ -307,11 +307,11 @@ func retriveUrlConnsOptions(murl string) (string, int, int, int, int) {
 			}
 			if vals.Has("max_idle_time") {
 				vIdleTime, _ = strconv.Atoi(vals.Get("max_idle_time"))
-				vals.Del("max_idle_time");
+				vals.Del("max_idle_time")
 			}
 			if vals.Has("max_life_time") {
 				vLifeTime, _ = strconv.Atoi(vals.Get("max_life_time"))
-				vals.Del("max_life_time");
+				vals.Del("max_life_time")
 			}
 			optsurl = vals.Encode()
 		}
@@ -1155,6 +1155,7 @@ func (m *dbMeta) doLookup(ctx Context, parent Ino, name string, inode *Ino, attr
 		}
 		*inode = nn.Inode
 		m.parseAttr(&nn.node, attr)
+		m.of.Update(nn.Inode, attr)
 		return nil
 	}))
 }
diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go
index d6973383..010b7cf2 100644
--- a/pkg/meta/tkv.go
+++ b/pkg/meta/tkv.go
@@ -897,6 +897,7 @@ func (m *kvMeta) doLookup(ctx Context, parent Ino, name string, inode *Ino, attr
 	a, err := m.get(m.inodeKey(foundIno))
 	if a != nil {
 		m.parseAttr(a, attr)
+		m.of.Update(foundIno, attr)
 	} else if err == nil {
 		logger.Warnf("no attribute for inode %d (%d, %s)", foundIno, parent, name)
 		*attr = Attr{Typ: foundType}
diff --git a/pkg/object/sftp.go b/pkg/object/sftp.go
index b7ea087b..69f865a3 100644
--- a/pkg/object/sftp.go
+++ b/pkg/object/sftp.go
@@ -9,6 +9,13 @@ package object
 import (
 	"bytes"
 	"fmt"
+	"github.com/juicedata/juicefs/pkg/utils"
+	"github.com/pkg/errors"
+	"github.com/pkg/sftp"
+	"golang.org/x/crypto/ssh"
+	"golang.org/x/crypto/ssh/agent"
+	"golang.org/x/crypto/ssh/knownhosts"
+	"golang.org/x/term"
 	"io"
 	"math/rand"
 	"net"
@@ -23,13 +30,6 @@ import (
 	"sync"
 	"syscall"
 	"time"
-
-	"github.com/juicedata/juicefs/pkg/utils"
-	"github.com/pkg/errors"
-	"github.com/pkg/sftp"
-	"golang.org/x/crypto/ssh"
-	"golang.org/x/crypto/ssh/agent"
-	"golang.org/x/term"
 )
 
 // conn encapsulates an ssh client and corresponding sftp client
@@ -526,10 +526,20 @@ func newSftp(endpoint, username, pass, token string) (ObjectStorage, error) {
 	if pass == "" {
 		auth = append(auth, ssh.KeyboardInteractive(sshInteractive))
 	}
+	var hostKeyCallback ssh.HostKeyCallback
+	if kn := os.Getenv("SSH_KNOWN_HOSTS"); kn != "" {
+		var err error
+		hostKeyCallback, err = knownhosts.New(kn)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		hostKeyCallback = ssh.InsecureIgnoreHostKey()
+	}
 
 	config := &ssh.ClientConfig{
 		User:            username,
-		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+		HostKeyCallback: hostKeyCallback,
 		Timeout:         time.Second * 3,
 		Auth:            auth,
 	}
diff --git a/pkg/sync/sync.go b/pkg/sync/sync.go
index 909cb2c6..7d2682ca 100644
--- a/pkg/sync/sync.go
+++ b/pkg/sync/sync.go
@@ -22,6 +22,7 @@ import (
 	"fmt"
 	"hash/crc32"
 	"io"
+	"math"
 	"os"
 	"path"
 	"runtime"
@@ -29,7 +30,6 @@ import (
 	"sync"
 	"time"
 
-	"github.com/juicedata/juicefs/pkg/chunk"
 	"github.com/juicedata/juicefs/pkg/object"
 	"github.com/juicedata/juicefs/pkg/utils"
 	"github.com/juju/ratelimit"
@@ -552,15 +552,43 @@ func (w *withProgress) Read(b []byte) (int, error) {
 	return n, err
 }
 
+func dynAlloc(size int) []byte {
+	zeros := utils.PowerOf2(size)
+	b := *dynPools[zeros].Get().(*[]byte)
+	if cap(b) < size {
+		panic(fmt.Sprintf("%d < %d", cap(b), size))
+	}
+	return b[:size]
+}
+
+func dynFree(b []byte) {
+	dynPools[utils.PowerOf2(cap(b))].Put(&b)
+}
+
+var dynPools []*sync.Pool
+
+func init() {
+	dynPools = make([]*sync.Pool, 33) // 1 - 8G
+	for i := 0; i < 33; i++ {
+		func(bits int) {
+			dynPools[i] = &sync.Pool{
+				New: func() interface{} {
+					b := make([]byte, 1<<bits)
+					return &b
+				},
+			}
+		}(i)
+	}
+}
+
 func doUploadPart(src, dst object.ObjectStorage, srckey string, off, size int64, key, uploadID string, num int, calChksum bool) (*object.Part, uint32, error) {
 	if limiter != nil {
 		limiter.Wait(size)
 	}
 	start := time.Now()
 	sz := size
-	p := chunk.NewOffPage(int(size))
-	defer p.Release()
-	data := p.Data
+	data := dynAlloc(int(size))
+	defer dynFree(data)
 	var part *object.Part
 	var chksum uint32
 	err := try(3, func() error {
@@ -726,7 +754,14 @@ func doCopyMultiple(src, dst object.ObjectStorage, key string, size int64, uploa
 	return chksum, nil
 }
 
-func copyData(src, dst object.ObjectStorage, key string, size int64, calChksum bool) (uint32, error) {
+func InitForCopyData() {
+	concurrent = make(chan int, 10)
+	progress := utils.NewProgress(true)
+	copied = progress.AddCountSpinner("Copied objects")
+	copiedBytes = progress.AddByteSpinner("Copied bytes")
+}
+
+func CopyData(src, dst object.ObjectStorage, key string, size int64, calChksum bool) (uint32, error) {
 	start := time.Now()
 	var err error
 	var srcChksum uint32
@@ -824,7 +859,7 @@ func worker(tasks <-chan object.Object, src, dst object.ObjectStorage, config *C
 					logger.Errorf("copy link failed: %s", err)
 				}
 			} else {
-				srcChksum, err = copyData(src, dst, key, obj.Size(), config.CheckAll || config.CheckNew)
+				srcChksum, err = CopyData(src, dst, key, obj.Size(), config.CheckAll || config.CheckNew)
 			}
 
 			if err == nil && (config.CheckAll || config.CheckNew) {
@@ -1247,8 +1282,14 @@ func listCommonPrefix(store object.ObjectStorage, prefix string, cp chan object.
 	var nextToken string
 	var marker string
 	var hasMore bool
+	var thisListMaxResults int64 = maxResults
+	if strings.HasPrefix(store.String(), "file://") || strings.HasPrefix(store.String(), "nfs://") ||
+		strings.HasPrefix(store.String(), "gluster://") || strings.HasPrefix(store.String(), "jfs://") ||
+		strings.HasPrefix(store.String(), "hdfs://") || strings.HasPrefix(store.String(), "webdav://") {
+		thisListMaxResults = math.MaxInt64
+	}
 	for {
-		objs, hasMore, nextToken, err = store.List(prefix, marker, nextToken, "/", maxResults, followLink)
+		objs, hasMore, nextToken, err = store.List(prefix, marker, nextToken, "/", thisListMaxResults, followLink)
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/utils/alloc.go b/pkg/utils/alloc.go
index 74a7f864..79988ea7 100644
--- a/pkg/utils/alloc.go
+++ b/pkg/utils/alloc.go
@@ -28,7 +28,7 @@ var used int64
 
 // Alloc returns size bytes memory from Go heap.
 func Alloc(size int) []byte {
-	zeros := powerOf2(size)
+	zeros := PowerOf2(size)
 	b := *pools[zeros].Get().(*[]byte)
 	if cap(b) < size {
 		panic(fmt.Sprintf("%d < %d", cap(b), size))
@@ -41,7 +41,7 @@ func Alloc(size int) []byte {
 func Free(b []byte) {
 	// buf could be zero length
 	atomic.AddInt64(&used, -int64(cap(b)))
-	pools[powerOf2(cap(b))].Put(&b)
+	pools[PowerOf2(cap(b))].Put(&b)
 }
 
 // AllocMemory returns the allocated memory
@@ -51,7 +51,7 @@ func AllocMemory() int64 {
 
 var pools []*sync.Pool
 
-func powerOf2(s int) int {
+func PowerOf2(s int) int {
 	var bits int
 	var p int = 1
 	for p < s {
diff --git a/pkg/vfs/accesslog.go b/pkg/vfs/accesslog.go
index 94863244..395d1738 100644
--- a/pkg/vfs/accesslog.go
+++ b/pkg/vfs/accesslog.go
@@ -21,6 +21,8 @@ import (
 	"sync"
 	"syscall"
 	"time"
+	"strconv"
+	"strings"
 
 	"github.com/juicedata/juicefs/pkg/utils"
 	"github.com/prometheus/client_golang/prometheus"
@@ -74,7 +76,14 @@ func logit(ctx Context, method string, err syscall.Errno, format string, args ..
 	if len(readers) == 0 && used < time.Second*10 {
 		return
 	}
-
+	for i, a := range args {
+		switch v := a.(type) {
+		case string:
+			if !strconv.CanBackquote(v) {
+				args[i] = strings.Trim(strconv.Quote(v), "\"")
+			}
+		}
+	}
 	cmd := fmt.Sprintf(method+" "+format, args...)
 	t := utils.Now()
 	ts := t.Format("2006.01.02 15:04:05.000000")
diff --git a/pkg/vfs/backup.go b/pkg/vfs/backup.go
index 5bf28153..70382823 100644
--- a/pkg/vfs/backup.go
+++ b/pkg/vfs/backup.go
@@ -18,9 +18,13 @@ package vfs
 
 import (
 	"compress/gzip"
+	"errors"
 	"io"
 	"os"
+	"path/filepath"
 	"sort"
+	"strings"
+	"syscall"
 	"time"
 
 	"github.com/juicedata/juicefs/pkg/meta"
@@ -92,7 +96,17 @@ func Backup(m meta.Meta, blob object.ObjectStorage, interval time.Duration, skip
 
 func backup(m meta.Meta, blob object.ObjectStorage, now time.Time, fast, skipTrash bool) (string, error) {
 	name := "dump-" + now.UTC().Format("2006-01-02-150405") + ".json.gz"
-	fp, err := os.CreateTemp("", "juicefs-meta-*")
+	localDir := os.TempDir()
+	if !strings.HasSuffix(localDir, "/") {
+		localDir += "/"
+	}
+	fp, err := os.Create(filepath.Join(localDir, "meta", name))
+	if errors.Is(err, syscall.ENOENT) {
+		if err = os.MkdirAll(filepath.Join(localDir, "meta"), 0755); err != nil {
+			return "", err
+		}
+		fp, err = os.Create(filepath.Join(localDir, "meta", name))
+	}
 	if err != nil {
 		return "", err
 	}
@@ -108,11 +122,19 @@ func backup(m meta.Meta, blob object.ObjectStorage, now time.Time, fast, skipTra
 	if err != nil {
 		return "", err
 	}
-	if _, err = fp.Seek(0, io.SeekStart); err != nil {
+	size, err := fp.Seek(0, io.SeekCurrent)
+	if err != nil {
 		return "", err
 	}
+
 	fpath := "meta/" + name
-	return blob.String() + fpath, blob.Put(fpath, fp)
+	disk, err := object.CreateStorage("file", localDir, "", "", "")
+	if err != nil {
+		return "", err
+	}
+	osync.InitForCopyData()
+	_, err = osync.CopyData(disk, blob, fpath, size, true)
+	return blob.String() + fpath, err
 }
 
 func cleanupBackups(blob object.ObjectStorage, now time.Time) {
diff --git a/pkg/vfs/reader.go b/pkg/vfs/reader.go
index 89da44b8..e8a858b5 100644
--- a/pkg/vfs/reader.go
+++ b/pkg/vfs/reader.go
@@ -620,6 +620,12 @@ func (f *fileReader) waitForIO(ctx meta.Context, reqs []*req, buf []byte) (int,
 }
 
 func (f *fileReader) Read(ctx meta.Context, offset uint64, buf []byte) (int, syscall.Errno) {
+	if f.r.readBufferUsed() > f.r.bufferSize {
+		time.Sleep(time.Millisecond * 10)             // slow down
+		for f.r.readBufferUsed() > f.r.bufferSize*2 { // readahead uses 80% of buffer, stop here to avoid OOM
+			time.Sleep(time.Millisecond * 100)
+		}
+	}
 	f.Lock()
 	defer f.Unlock()
 	f.acquire()
@@ -689,6 +695,7 @@ type dataReader struct {
 	store          chunk.ChunkStore
 	files          map[Ino]*fileReader
 	blockSize      uint64
+	bufferSize     int64
 	readAheadMax   uint64
 	readAheadTotal uint64
 	maxRequests    int
@@ -709,6 +716,7 @@ func NewDataReader(conf *Config, m meta.Meta, store chunk.ChunkStore) DataReader
 		store:          store,
 		files:          make(map[Ino]*fileReader),
 		blockSize:      uint64(conf.Chunk.BlockSize),
+		bufferSize:     int64(conf.Chunk.BufferSize),
 		readAheadTotal: uint64(readAheadTotal),
 		readAheadMax:   uint64(readAheadMax),
 		maxRequests:    readAheadMax/conf.Chunk.BlockSize*readSessions + 1,
diff --git a/pkg/vfs/vfs.go b/pkg/vfs/vfs.go
index 63bdf130..0ca2f099 100644
--- a/pkg/vfs/vfs.go
+++ b/pkg/vfs/vfs.go
@@ -1220,11 +1220,7 @@ type VFS struct {
 	modM       sync.Mutex
 	modifiedAt map[Ino]time.Time
 
-	handlersGause  prometheus.GaugeFunc
-	usedBufferSize prometheus.GaugeFunc
-	storeCacheSize prometheus.GaugeFunc
-	readBufferUsed prometheus.GaugeFunc
-	registry       *prometheus.Registry
+	registry *prometheus.Registry
 }
 
 func NewVFS(conf *Config, m meta.Meta, store chunk.ChunkStore, registerer prometheus.Registerer, registry *prometheus.Registry) *VFS {
@@ -1324,7 +1320,7 @@ func initVFSMetrics(v *VFS, writer DataWriter, reader DataReader, registerer pro
 	if registerer == nil {
 		return
 	}
-	v.handlersGause = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
+	handlersGause := prometheus.NewGaugeFunc(prometheus.GaugeOpts{
 		Name: "fuse_open_handlers",
 		Help: "number of open files and directories.",
 	}, func() float64 {
@@ -1332,7 +1328,12 @@ func initVFSMetrics(v *VFS, writer DataWriter, reader DataReader, registerer pro
 		defer v.hanleM.Unlock()
 		return float64(len(v.handles))
 	})
-	v.usedBufferSize = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
+	_ = registerer.Register(handlersGause)
+	InitMemoryBufferMetrics(writer, reader, registerer)
+}
+
+func InitMemoryBufferMetrics(writer DataWriter, reader DataReader, registerer prometheus.Registerer) {
+	usedBufferSize := prometheus.NewGaugeFunc(prometheus.GaugeOpts{
 		Name: "used_buffer_size_bytes",
 		Help: "size of currently used buffer.",
 	}, func() float64 {
@@ -1341,7 +1342,7 @@ func initVFSMetrics(v *VFS, writer DataWriter, reader DataReader, registerer pro
 		}
 		return 0.0
 	})
-	v.storeCacheSize = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
+	storeCacheSize := prometheus.NewGaugeFunc(prometheus.GaugeOpts{
 		Name: "store_cache_size_bytes",
 		Help: "size of store cache.",
 	}, func() float64 {
@@ -1350,7 +1351,7 @@ func initVFSMetrics(v *VFS, writer DataWriter, reader DataReader, registerer pro
 		}
 		return 0.0
 	})
-	v.readBufferUsed = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
+	readBufferMetric := prometheus.NewGaugeFunc(prometheus.GaugeOpts{
 		Name: "used_read_buffer_size_bytes",
 		Help: "size of currently used buffer for read",
 	}, func() float64 {
@@ -1359,10 +1360,9 @@ func initVFSMetrics(v *VFS, writer DataWriter, reader DataReader, registerer pro
 		}
 		return 0.0
 	})
-	_ = registerer.Register(v.handlersGause)
-	_ = registerer.Register(v.usedBufferSize)
-	_ = registerer.Register(v.storeCacheSize)
-	_ = registerer.Register(v.readBufferUsed)
+	_ = registerer.Register(usedBufferSize)
+	_ = registerer.Register(storeCacheSize)
+	_ = registerer.Register(readBufferMetric)
 }
 
 func InitMetrics(registerer prometheus.Registerer) {
diff --git a/sdk/java/libjfs/main.go b/sdk/java/libjfs/main.go
index f50e24e9..e41e21ae 100644
--- a/sdk/java/libjfs/main.go
+++ b/sdk/java/libjfs/main.go
@@ -79,8 +79,8 @@ import (
 
 var (
 	filesLock  sync.Mutex
-	openFiles  = make(map[int]*fwrapper)
-	nextHandle = 1
+	openFiles  = make(map[int32]*fwrapper)
+	nextHandle = int32(1)
 
 	fslock       sync.Mutex
 	handlers           = make(map[int64]*wrapper)
@@ -94,6 +94,17 @@ var (
 
 	userGroupCache = make(map[string]map[string][]string) // name -> (user -> groups)
 
+	MaxDeletes = meta.RmrDefaultThreads
+	caller     = CALLER_JAVA
+)
+
+const (
+	CALLER_JAVA = iota
+	CALLER_PYTHON
+)
+
+const (
+	BEHAVIOR_HADOOP = "Hadoop"
 )
 
 const (
@@ -113,7 +124,7 @@ const (
 	ENOTSUP   = -0x5f
 )
 
-func errno(err error) int {
+func errno(err error) int32 {
 	if err == nil {
 		return 0
 	}
@@ -156,7 +167,7 @@ func errno(err error) int {
 		return ENOTSUP
 	default:
 		logger.Warnf("unknown errno %d: %s", eno, err)
-		return -int(eno)
+		return -int32(eno)
 	}
 }
 
@@ -207,10 +218,12 @@ func jfs_set_logger(cb unsafe.Pointer) {
 	}
 }
 
-func (w *wrapper) withPid(pid int) meta.Context {
+func (w *wrapper) withPid(pid int64) meta.Context {
 	// mapping Java Thread ID to global one
 	ctx := meta.NewContext(w.ctx.Pid()*1000+uint32(pid), w.ctx.Uid(), w.ctx.Gids())
-	ctx.WithValue(meta.CtxKey("behavior"), "Hadoop")
+	if caller == CALLER_JAVA {
+		ctx.WithValue(meta.CtxKey("behavior"), BEHAVIOR_HADOOP)
+	}
 	return ctx
 }
 
@@ -269,7 +282,7 @@ type fwrapper struct {
 	w *wrapper
 }
 
-func nextFileHandle(f *fs.File, w *wrapper) int {
+func nextFileHandle(f *fs.File, w *wrapper) int32 {
 	filesLock.Lock()
 	defer filesLock.Unlock()
 	for i := nextHandle; ; i++ {
@@ -281,7 +294,7 @@ func nextFileHandle(f *fs.File, w *wrapper) int {
 	}
 }
 
-func freeHandle(fd int) {
+func freeHandle(fd int32) {
 	filesLock.Lock()
 	defer filesLock.Unlock()
 	f := openFiles[fd]
@@ -336,6 +349,7 @@ type javaConf struct {
 	PushAuth          string `json:"pushAuth"`
 	PushLabels        string `json:"pushLabels"`
 	PushGraphite      string `json:"pushGraphite"`
+	Caller            int    `json:"caller"`
 }
 
 func getOrCreate(name, user, group, superuser, supergroup string, f func() *fs.FileSystem) int64 {
@@ -467,6 +481,11 @@ func jfs_init(cname, jsonConf, user, group, superuser, supergroup *C.char) int64
 			utils.SetLogLevel(logrus.WarnLevel)
 		}
 
+		caller = jConf.Caller
+		if jConf.MaxDeletes > 0 {
+			MaxDeletes = jConf.MaxDeletes
+		}
+
 		metaConf := meta.DefaultConf()
 		metaConf.Retries = jConf.IORetries
 		metaConf.MaxDeletes = jConf.MaxDeletes
@@ -712,7 +731,7 @@ func jfs_getGroups(name, user string) string {
 }
 
 //export jfs_term
-func jfs_term(pid int, h int64) int {
+func jfs_term(pid int64, h int64) int32 {
 	w := F(h)
 	if w == nil {
 		return 0
@@ -721,7 +740,7 @@ func jfs_term(pid int, h int64) int {
 	// sync all open files
 	filesLock.Lock()
 	var m sync.WaitGroup
-	var toClose []int
+	var toClose []int32
 	for fd, f := range openFiles {
 		if f.w == w {
 			m.Add(1)
@@ -770,7 +789,7 @@ func jfs_term(pid int, h int64) int {
 }
 
 //export jfs_open
-func jfs_open(pid int, h int64, cpath *C.char, lenPtr uintptr, flags int) int {
+func jfs_open(pid int64, h int64, cpath *C.char, lenPtr uintptr, flags int32) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -793,16 +812,16 @@ func jfs_open(pid int, h int64, cpath *C.char, lenPtr uintptr, flags int) int {
 }
 
 //export jfs_access
-func jfs_access(pid int, h int64, cpath *C.char, flags int) int {
+func jfs_access(pid int64, h int64, cpath *C.char, flags int64) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
 	}
-	return errno(w.Access(w.withPid(pid), C.GoString(cpath), flags))
+	return errno(w.Access(w.withPid(pid), C.GoString(cpath), int(flags)))
 }
 
 //export jfs_create
-func jfs_create(pid int, h int64, cpath *C.char, mode uint16, umask uint16) int {
+func jfs_create(pid int64, h int64, cpath *C.char, mode uint16, umask uint16) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -820,7 +839,7 @@ func jfs_create(pid int, h int64, cpath *C.char, mode uint16, umask uint16) int
 }
 
 //export jfs_mkdir
-func jfs_mkdir(pid int, h int64, cpath *C.char, mode uint16, umask uint16) int {
+func jfs_mkdir(pid int64, h int64, cpath *C.char, mode uint16, umask uint16) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -834,13 +853,13 @@ func jfs_mkdir(pid int, h int64, cpath *C.char, mode uint16, umask uint16) int {
 }
 
 //export jfs_mkdirAll
-func jfs_mkdirAll(pid int, h int64, cpath *C.char, mode, umask uint16) int {
+func jfs_mkdirAll(pid int64, h int64, cpath *C.char, mode, umask uint16, existOK bool) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
 	}
 	path := C.GoString(cpath)
-	err := errno(w.MkdirAll(w.withPid(pid), path, mode, umask))
+	err := errno(w.MkdirAll0(w.withPid(pid), path, mode, umask, existOK))
 	if err == 0 && w.ctx.Uid() == 0 && w.user != w.superuser {
 		// belongs to supergroup
 		if err := setOwner(w, w.withPid(pid), path, w.user, ""); err != 0 {
@@ -851,7 +870,7 @@ func jfs_mkdirAll(pid int, h int64, cpath *C.char, mode, umask uint16) int {
 }
 
 //export jfs_delete
-func jfs_delete(pid int, h int64, cpath *C.char) int {
+func jfs_delete(pid int64, h int64, cpath *C.char) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -860,16 +879,16 @@ func jfs_delete(pid int, h int64, cpath *C.char) int {
 }
 
 //export jfs_rmr
-func jfs_rmr(pid int, h int64, cpath *C.char) int {
+func jfs_rmr(pid int64, h int64, cpath *C.char) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
 	}
-	return errno(w.Rmr(w.withPid(pid), C.GoString(cpath)))
+	return errno(w.Rmr(w.withPid(pid), C.GoString(cpath), MaxDeletes))
 }
 
 //export jfs_rename
-func jfs_rename(pid int, h int64, oldpath *C.char, newpath *C.char) int {
+func jfs_rename(pid int64, h int64, oldpath *C.char, newpath *C.char) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -878,7 +897,7 @@ func jfs_rename(pid int, h int64, oldpath *C.char, newpath *C.char) int {
 }
 
 //export jfs_truncate
-func jfs_truncate(pid int, h int64, path *C.char, length uint64) int {
+func jfs_truncate(pid int64, h int64, path *C.char, length uint64) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -887,7 +906,7 @@ func jfs_truncate(pid int, h int64, path *C.char, length uint64) int {
 }
 
 //export jfs_setXattr
-func jfs_setXattr(pid int, h int64, path *C.char, name *C.char, value uintptr, vlen int, mode int) int {
+func jfs_setXattr(pid int64, h int64, path *C.char, name *C.char, value uintptr, vlen, mode int32) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -903,7 +922,7 @@ func jfs_setXattr(pid int, h int64, path *C.char, name *C.char, value uintptr, v
 }
 
 //export jfs_setXattr2
-func jfs_setXattr2(pid int, h int64, path *C.char, name *C.char, value *C.char, mode int) int {
+func jfs_setXattr2(pid int64, h int64, path *C.char, name *C.char, value *C.char, mode int64) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -919,7 +938,7 @@ func jfs_setXattr2(pid int, h int64, path *C.char, name *C.char, value *C.char,
 }
 
 //export jfs_getXattr
-func jfs_getXattr(pid int, h int64, path *C.char, name *C.char, buf uintptr, bufsize int) int {
+func jfs_getXattr(pid int64, h int64, path *C.char, name *C.char, buf uintptr, bufsize int32) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -928,15 +947,15 @@ func jfs_getXattr(pid int, h int64, path *C.char, name *C.char, buf uintptr, buf
 	if err != 0 {
 		return errno(err)
 	}
-	if len(buff) >= bufsize {
+	if int32(len(buff)) >= bufsize {
 		return bufsize
 	}
 	copy(toBuf(buf, bufsize), buff)
-	return len(buff)
+	return int32(len(buff))
 }
 
 //export jfs_getXattr2
-func jfs_getXattr2(pid int, h int64, path *C.char, name *C.char, value **C.char) int {
+func jfs_getXattr2(pid int64, h int64, path *C.char, name *C.char, value **C.char) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -949,7 +968,7 @@ func jfs_getXattr2(pid int, h int64, path *C.char, name *C.char, value **C.char)
 }
 
 //export jfs_listXattr
-func jfs_listXattr(pid int, h int64, path *C.char, buf uintptr, bufsize int) int {
+func jfs_listXattr(pid int64, h int64, path *C.char, buf uintptr, bufsize int32) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -958,15 +977,15 @@ func jfs_listXattr(pid int, h int64, path *C.char, buf uintptr, bufsize int) int
 	if err != 0 {
 		return errno(err)
 	}
-	if len(buff) >= bufsize {
+	if int32(len(buff)) >= bufsize {
 		return bufsize
 	}
 	copy(toBuf(buf, bufsize), buff)
-	return len(buff)
+	return int32(len(buff))
 }
 
 //export jfs_listXattr2
-func jfs_listXattr2(pid int, h int64, path *C.char, value **C.char, size *int) int {
+func jfs_listXattr2(pid int64, h int64, path *C.char, value **C.char, size *int) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -980,7 +999,7 @@ func jfs_listXattr2(pid int, h int64, path *C.char, value **C.char, size *int) i
 }
 
 //export jfs_removeXattr
-func jfs_removeXattr(pid int, h int64, path *C.char, name *C.char) int {
+func jfs_removeXattr(pid int64, h int64, path *C.char, name *C.char) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -989,7 +1008,7 @@ func jfs_removeXattr(pid int, h int64, path *C.char, name *C.char) int {
 }
 
 //export jfs_getfacl
-func jfs_getfacl(pid int, h int64, path *C.char, acltype int, buf uintptr, blen int) int {
+func jfs_getfacl(pid int64, h int64, path *C.char, acltype int32, buf uintptr, blen int32) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1021,11 +1040,11 @@ func jfs_getfacl(pid int, h int64, path *C.char, acltype int, buf uintptr, blen
 		wb.Put8(0)
 		wb.Put16(entry.Perm)
 	}
-	return int(off)
+	return int32(off)
 }
 
 //export jfs_setfacl
-func jfs_setfacl(pid int, h int64, path *C.char, acltype int, buf uintptr, alen int) int {
+func jfs_setfacl(pid int64, h int64, path *C.char, acltype int32, buf uintptr, alen int32) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1054,7 +1073,7 @@ func jfs_setfacl(pid int, h int64, path *C.char, acltype int, buf uintptr, alen
 }
 
 //export jfs_link
-func jfs_link(pid int, h int64, src *C.char, dst *C.char) int {
+func jfs_link(pid int64, h int64, src *C.char, dst *C.char) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1063,7 +1082,7 @@ func jfs_link(pid int, h int64, src *C.char, dst *C.char) int {
 }
 
 //export jfs_symlink
-func jfs_symlink(pid int, h int64, target_ *C.char, link_ *C.char) int {
+func jfs_symlink(pid int64, h int64, target_ *C.char, link_ *C.char) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1080,7 +1099,7 @@ func jfs_symlink(pid int, h int64, target_ *C.char, link_ *C.char) int {
 }
 
 //export jfs_readlink
-func jfs_readlink(pid int, h int64, link *C.char, buf uintptr, bufsize int) int {
+func jfs_readlink(pid int64, h int64, link *C.char, buf uintptr, bufsize int32) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1089,17 +1108,17 @@ func jfs_readlink(pid int, h int64, link *C.char, buf uintptr, bufsize int) int
 	if err != 0 {
 		return errno(err)
 	}
-	if len(target)+1 >= bufsize {
+	if int32(len(target)+1) >= bufsize {
 		target = target[:bufsize-1]
 	}
 	wb := utils.NewNativeBuffer(toBuf(buf, bufsize))
 	wb.Put(target)
 	wb.Put8(0)
-	return len(target)
+	return int32(len(target))
 }
 
 // mode:4 length:8 mtime:8 atime:8 user:50 group:50
-func fill_stat(w *wrapper, wb *utils.Buffer, st *fs.FileStat) int {
+func fill_stat(w *wrapper, wb *utils.Buffer, st *fs.FileStat) int32 {
 	wb.Put32(uint32(st.Mode()))
 	wb.Put64(uint64(st.Size()))
 	wb.Put64(uint64(st.Mtime()))
@@ -1110,11 +1129,11 @@ func fill_stat(w *wrapper, wb *utils.Buffer, st *fs.FileStat) int {
 	group := w.gid2name(uint32(st.Gid()))
 	wb.Put([]byte(group))
 	wb.Put8(0)
-	return 30 + len(user) + len(group)
+	return 30 + int32(len(user)) + int32(len(group))
 }
 
 //export jfs_stat1
-func jfs_stat1(pid int, h int64, cpath *C.char, buf uintptr) int {
+func jfs_stat1(pid int64, h int64, cpath *C.char, buf uintptr) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1127,7 +1146,7 @@ func jfs_stat1(pid int, h int64, cpath *C.char, buf uintptr) int {
 }
 
 //export jfs_lstat1
-func jfs_lstat1(pid int, h int64, cpath *C.char, buf uintptr) int {
+func jfs_lstat1(pid int64, h int64, cpath *C.char, buf uintptr) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1152,7 +1171,7 @@ func attrToInfo(fi *fs.FileStat, info *C.fileInfo) {
 }
 
 //export jfs_stat
-func jfs_stat(pid int, h int64, cpath *C.char, info *C.fileInfo) int {
+func jfs_stat(pid int64, h int64, cpath *C.char, info *C.fileInfo) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1167,7 +1186,7 @@ func jfs_stat(pid int, h int64, cpath *C.char, info *C.fileInfo) int {
 }
 
 //export jfs_lstat
-func jfs_lstat(pid int, h int64, cpath *C.char, info *C.fileInfo) int {
+func jfs_lstat(pid int64, h int64, cpath *C.char, info *C.fileInfo) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1182,7 +1201,7 @@ func jfs_lstat(pid int, h int64, cpath *C.char, info *C.fileInfo) int {
 }
 
 //export jfs_summary
-func jfs_summary(pid int, h int64, cpath *C.char, buf uintptr) int {
+func jfs_summary(pid int64, h int64, cpath *C.char, buf uintptr) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1205,7 +1224,7 @@ func jfs_summary(pid int, h int64, cpath *C.char, buf uintptr) int {
 }
 
 //export jfs_statvfs
-func jfs_statvfs(pid int, h int64, buf uintptr) int {
+func jfs_statvfs(pid int64, h int64, buf uintptr) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1218,7 +1237,7 @@ func jfs_statvfs(pid int, h int64, buf uintptr) int {
 }
 
 //export jfs_chmod
-func jfs_chmod(pid int, h int64, cpath *C.char, mode C.mode_t) int {
+func jfs_chmod(pid int64, h int64, cpath *C.char, mode C.mode_t) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1232,7 +1251,7 @@ func jfs_chmod(pid int, h int64, cpath *C.char, mode C.mode_t) int {
 }
 
 //export jfs_chown
-func jfs_chown(pid int, h int64, cpath *C.char, uid uint32, gid uint32) int {
+func jfs_chown(pid int64, h int64, cpath *C.char, uid uint32, gid uint32) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1245,7 +1264,7 @@ func jfs_chown(pid int, h int64, cpath *C.char, uid uint32, gid uint32) int {
 }
 
 //export jfs_utime
-func jfs_utime(pid int, h int64, cpath *C.char, mtime, atime int64) int {
+func jfs_utime(pid int64, h int64, cpath *C.char, mtime, atime int64) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1259,7 +1278,7 @@ func jfs_utime(pid int, h int64, cpath *C.char, mtime, atime int64) int {
 }
 
 //export jfs_setOwner
-func jfs_setOwner(pid int, h int64, cpath *C.char, owner *C.char, group *C.char) int {
+func jfs_setOwner(pid int64, h int64, cpath *C.char, owner *C.char, group *C.char) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1267,7 +1286,7 @@ func jfs_setOwner(pid int, h int64, cpath *C.char, owner *C.char, group *C.char)
 	return setOwner(w, w.withPid(pid), C.GoString(cpath), C.GoString(owner), C.GoString(group))
 }
 
-func setOwner(w *wrapper, ctx meta.Context, path string, owner, group string) int {
+func setOwner(w *wrapper, ctx meta.Context, path string, owner, group string) int32 {
 	f, err := w.Open(ctx, path, 0)
 	if err != 0 {
 		return errno(err)
@@ -1286,18 +1305,18 @@ func setOwner(w *wrapper, ctx meta.Context, path string, owner, group string) in
 }
 
 //export jfs_listdir
-func jfs_listdir(pid int, h int64, cpath *C.char, offset int, buf uintptr, bufsize int) int {
+func jfs_listdir(pid int64, h int64, cpath *C.char, offset int64, buf uintptr, bufsize int32) int32 {
 	var ctx meta.Context
 	var f *fs.File
 	var w *wrapper
 	if offset > 0 {
 		filesLock.Lock()
-		fw := openFiles[int(h)]
+		fw := openFiles[int32(h)]
 		filesLock.Unlock()
 		if fw == nil {
 			return EINVAL
 		}
-		freeHandle(int(h))
+		freeHandle(int32(h))
 		w = fw.w
 		f = fw.File
 		ctx = w.withPid(pid)
@@ -1318,7 +1337,7 @@ func jfs_listdir(pid int, h int64, cpath *C.char, offset int, buf uintptr, bufsi
 		}
 	}
 
-	es, err := f.ReaddirPlus(ctx, offset)
+	es, err := f.ReaddirPlus(ctx, int(offset))
 	if err != 0 {
 		return errno(err)
 	}
@@ -1328,7 +1347,7 @@ func jfs_listdir(pid int, h int64, cpath *C.char, offset int, buf uintptr, bufsi
 		if wb.Left() < 1+len(d.Name)+1+130+8 {
 			wb.Put32(uint32(len(es) - i))
 			wb.Put32(uint32(nextFileHandle(f, w)))
-			return bufsize - wb.Left() - 8
+			return bufsize - int32(wb.Left()) - 8
 		}
 		wb.Put8(byte(len(d.Name)))
 		wb.Put(d.Name)
@@ -1336,11 +1355,11 @@ func jfs_listdir(pid int, h int64, cpath *C.char, offset int, buf uintptr, bufsi
 		header[0] = uint8(fill_stat(w, wb, fs.AttrToFileInfo(d.Inode, d.Attr)))
 	}
 	wb.Put32(0)
-	return bufsize - wb.Left() - 4
+	return bufsize - int32(wb.Left()) - 4
 }
 
 //export jfs_listdir2
-func jfs_listdir2(pid int, h int64, cpath *C.char, plus bool, buf **byte, size *int) int {
+func jfs_listdir2(pid int64, h int64, cpath *C.char, plus bool, buf **byte, size *int64) int32 {
 	var ctx meta.Context
 	var f *fs.File
 	w := F(h)
@@ -1365,7 +1384,7 @@ func jfs_listdir2(pid int, h int64, cpath *C.char, plus bool, buf **byte, size *
 			return errno(err)
 		}
 		for _, e := range es {
-			*size += 2 + len(e.Name) + 4*11
+			*size += 2 + int64(len(e.Name)) + 4*11
 		}
 		*buf = (*byte)(C.malloc(C.size_t(*size)))
 		out := utils.FromBuffer(unsafe.Slice(*buf, *size))
@@ -1388,7 +1407,7 @@ func jfs_listdir2(pid int, h int64, cpath *C.char, plus bool, buf **byte, size *
 			return errno(err)
 		}
 		for _, e := range es {
-			*size += 2 + len(e.Name())
+			*size += 2 + int64(len(e.Name()))
 		}
 		*buf = (*byte)(C.malloc(C.size_t(*size)))
 		out := utils.FromBuffer(unsafe.Slice(*buf, *size))
@@ -1400,12 +1419,12 @@ func jfs_listdir2(pid int, h int64, cpath *C.char, plus bool, buf **byte, size *
 	return 0
 }
 
-func toBuf(s uintptr, sz int) []byte {
+func toBuf(s uintptr, sz int32) []byte {
 	return (*[1 << 30]byte)(unsafe.Pointer(s))[:sz:sz]
 }
 
 //export jfs_concat
-func jfs_concat(pid int, h int64, _dst *C.char, buf uintptr, bufsize int) int {
+func jfs_concat(pid int64, h int64, _dst *C.char, buf uintptr, bufsize int32) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1464,7 +1483,7 @@ func jfs_concat(pid int, h int64, _dst *C.char, buf uintptr, bufsize int) int {
 // TODO: implement real clone
 
 //export jfs_clone
-func jfs_clone(pid int, h int64, _src *C.char, _dst *C.char) int {
+func jfs_clone(pid int64, h int64, _src *C.char, _dst *C.char) int32 {
 	w := F(h)
 	if w == nil {
 		return EINVAL
@@ -1492,12 +1511,12 @@ func jfs_clone(pid int, h int64, _src *C.char, _dst *C.char) int {
 }
 
 //export jfs_lseek
-func jfs_lseek(pid, fd int, offset int64, whence int) int64 {
+func jfs_lseek(pid int64, fd int32, offset int64, whence int64) int64 {
 	filesLock.Lock()
 	f, ok := openFiles[fd]
 	if ok {
 		filesLock.Unlock()
-		off, _ := f.Seek(f.w.withPid(pid), offset, whence)
+		off, _ := f.Seek(f.w.withPid(pid), offset, int(whence))
 		return off
 	}
 	filesLock.Unlock()
@@ -1505,7 +1524,7 @@ func jfs_lseek(pid, fd int, offset int64, whence int) int64 {
 }
 
 //export jfs_read
-func jfs_read(pid, fd int, cbuf uintptr, count int) int {
+func jfs_read(pid int64, fd int32, cbuf uintptr, count int32) int32 {
 	filesLock.Lock()
 	f, ok := openFiles[fd]
 	if !ok {
@@ -1514,16 +1533,16 @@ func jfs_read(pid, fd int, cbuf uintptr, count int) int {
 	}
 	filesLock.Unlock()
 
-	n, err := f.Read(f.w.withPid(pid), toBuf(cbuf, count))
+	n, err := f.Read(f.w.withPid(pid), toBuf(cbuf, int32(count)))
 	if err != nil && err != io.EOF {
 		logger.Errorf("read %s: %s", f.Name(), err)
 		return errno(err)
 	}
-	return n
+	return int32(n)
 }
 
 //export jfs_pread
-func jfs_pread(pid, fd int, cbuf uintptr, count C.size_t, offset C.off_t) int {
+func jfs_pread(pid int64, fd int32, cbuf uintptr, count int32, offset int64) int32 {
 	filesLock.Lock()
 	f, ok := openFiles[fd]
 	if !ok {
@@ -1535,16 +1554,16 @@ func jfs_pread(pid, fd int, cbuf uintptr, count C.size_t, offset C.off_t) int {
 	if count > (1 << 30) {
 		count = 1 << 30
 	}
-	n, err := f.Pread(f.w.withPid(pid), toBuf(cbuf, int(count)), int64(offset))
+	n, err := f.Pread(f.w.withPid(pid), toBuf(cbuf, count), offset)
 	if err != nil && err != io.EOF {
 		logger.Errorf("read %s: %s", f.Name(), err)
 		return errno(err)
 	}
-	return n
+	return int32(n)
 }
 
 //export jfs_write
-func jfs_write(pid, fd int, cbuf uintptr, count C.size_t) int {
+func jfs_write(pid int64, fd int32, cbuf uintptr, count int32) int32 {
 	filesLock.Lock()
 	f, ok := openFiles[fd]
 	if !ok {
@@ -1553,17 +1572,17 @@ func jfs_write(pid, fd int, cbuf uintptr, count C.size_t) int {
 	}
 	filesLock.Unlock()
 
-	buf := toBuf(cbuf, int(count))
+	buf := toBuf(cbuf, count)
 	n, err := f.Write(f.w.withPid(pid), buf)
 	if err != 0 {
 		logger.Errorf("write %s: %s", f.Name(), err)
 		return errno(err)
 	}
-	return n
+	return int32(n)
 }
 
 //export jfs_pwrite
-func jfs_pwrite(pid, fd int, cbuf uintptr, count C.size_t, offset C.off_t) int {
+func jfs_pwrite(pid int64, fd int32, cbuf uintptr, count int32, offset int64) int32 {
 	filesLock.Lock()
 	f, ok := openFiles[fd]
 	if !ok {
@@ -1572,19 +1591,19 @@ func jfs_pwrite(pid, fd int, cbuf uintptr, count C.size_t, offset C.off_t) int {
 	}
 	filesLock.Unlock()
 
-	buf := toBuf(cbuf, int(count))
+	buf := toBuf(cbuf, count)
 	n, err := f.Pwrite(f.w.withPid(pid), buf, int64(offset))
 	if err != 0 {
 		logger.Errorf("pwrite %s: %s", f.Name(), err)
 		return errno(err)
 	}
-	return n
+	return int32(n)
 }
 
 //export jfs_ftruncate
-func jfs_ftruncate(pid, fd int, size uint64) int {
+func jfs_ftruncate(pid int64, fd int32, size uint64) int32 {
 	filesLock.Lock()
-	f, ok := openFiles[int(fd)]
+	f, ok := openFiles[fd]
 	filesLock.Unlock()
 	if !ok {
 		return EINVAL
@@ -1593,7 +1612,7 @@ func jfs_ftruncate(pid, fd int, size uint64) int {
 }
 
 //export jfs_flush
-func jfs_flush(pid, fd int) int {
+func jfs_flush(pid int64, fd int32) int32 {
 	filesLock.Lock()
 	f, ok := openFiles[fd]
 	if !ok {
@@ -1606,7 +1625,7 @@ func jfs_flush(pid, fd int) int {
 }
 
 //export jfs_fsync
-func jfs_fsync(pid, fd int) int {
+func jfs_fsync(pid int64, fd int32) int32 {
 	filesLock.Lock()
 	f, ok := openFiles[fd]
 	if !ok {
@@ -1619,7 +1638,7 @@ func jfs_fsync(pid, fd int) int {
 }
 
 //export jfs_close
-func jfs_close(pid, fd int) int {
+func jfs_close(pid int64, fd int32) int32 {
 	filesLock.Lock()
 	f, ok := openFiles[fd]
 	filesLock.Unlock()
diff --git a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
index 9a1033b9..36618ecb 100644
--- a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
+++ b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
@@ -110,6 +110,7 @@ public class JuiceFileSystemImpl extends FileSystem {
   private long handle;
   private UserGroupInformation ugi;
   private String homeDirPrefix = "/user";
+  private String discoverNodesUrl;
   private static final Map<String, Map<String, String>> cachedHostsForName = new ConcurrentHashMap<>(); // (name -> (ip -> hostname))
   private static final Map<String, ConsistentHash<String>> hashForName = new ConcurrentHashMap<>(); // (name -> consistentHash)
   private static final Map<String, FileStatus> lastFileStatus = new ConcurrentHashMap<>();
@@ -452,6 +453,7 @@ public class JuiceFileSystemImpl extends FileSystem {
     } else {
       BgTaskUtil.register(name, handle);
     }
+    discoverNodesUrl = getConf(conf, "discover-nodes-url", null);
     homeDirPrefix = conf.get("dfs.user.home.dir.prefix", "/user");
     this.workingDir = getHomeDirectory();
 
@@ -804,27 +806,24 @@ public class JuiceFileSystemImpl extends FileSystem {
     }
   }
 
-  private void initCache(Configuration conf) {
+  private void initCache() {
     try {
-      String urls = getConf(conf, "discover-nodes-url", null);
-      if (urls != null) {
-        List<String> newNodes = discoverNodes(urls);
-        Map<String, String> newCachedHosts = new HashMap<>();
-        for (String newNode : newNodes) {
-          try {
-            newCachedHosts.put(InetAddress.getByName(newNode).getHostAddress(), newNode);
-          } catch (UnknownHostException e) {
-            LOG.warn("unknown host: " + newNode);
-          }
+      List<String> newNodes = discoverNodes(discoverNodesUrl);
+      Map<String, String> newCachedHosts = new HashMap<>();
+      for (String newNode : newNodes) {
+        try {
+          newCachedHosts.put(InetAddress.getByName(newNode).getHostAddress(), newNode);
+        } catch (UnknownHostException e) {
+          LOG.warn("unknown host: " + newNode);
         }
+      }
 
-        // if newCachedHosts are not changed, skip
-        if (!newCachedHosts.equals(cachedHostsForName.get(name))) {
-          List<String> ips = new ArrayList<>(newCachedHosts.keySet());
-          LOG.debug("update nodes to: " + String.join(",", ips));
-          hashForName.put(name, new ConsistentHash<>(100, ips));
-          cachedHostsForName.put(name, newCachedHosts);
-        }
+      // if newCachedHosts are not changed, skip
+      if (!newCachedHosts.equals(cachedHostsForName.get(name))) {
+        List<String> ips = new ArrayList<>(newCachedHosts.keySet());
+        LOG.debug("update nodes to: " + String.join(",", ips));
+        hashForName.put(name, new ConsistentHash<>(100, ips));
+        cachedHostsForName.put(name, newCachedHosts);
       }
     } catch (Throwable e) {
       LOG.warn("failed to discover nodes", e);
@@ -833,7 +832,9 @@ public class JuiceFileSystemImpl extends FileSystem {
 
   private List<String> discoverNodes(String urls) {
     LOG.debug("fetching nodes from {}", urls);
-    NodesFetcher fetcher = NodesFetcherBuilder.buildFetcher(urls, name, this);
+    Configuration newConf = new Configuration(getConf());
+    newConf.setBoolean("juicefs.internal-bg-task", true);
+    NodesFetcher fetcher = NodesFetcherBuilder.buildFetcher(urls, name, newConf);
     List<String> fetched = fetcher.fetchNodes(urls);
     if (fetched == null) {
       fetched = new ArrayList<>();
@@ -850,14 +851,11 @@ public class JuiceFileSystemImpl extends FileSystem {
 
     Map<String, String> cachedHosts = cachedHostsForName.get(name);
     ConsistentHash<String> hash = hashForName.get(name);
-    String host = "localhost";
-    if (cachedHosts != null && hash != null) {
-      host = cachedHosts.get(hash.get(code + "-" + index));
-    }
-    ns[0] = host + ":50010";
-    hs[0] = host;
-    for (int i = 1; i < cacheReplica; i++) {
-      String h = hash.get(code + "-" + (index + i));
+    for (int i = 0; i < cacheReplica; i++) {
+      String h = "localhost";
+      if (cachedHosts != null && hash != null) {
+        h = cachedHosts.getOrDefault(hash.get(code + "-" + (index + i)), "localhost");
+      }
       ns[i] = h + ":50010";
       hs[i] = h;
     }
@@ -880,16 +878,36 @@ public class JuiceFileSystemImpl extends FileSystem {
     return res;
   }
 
-  @Override
-  public BlockLocation[] getFileBlockLocations(FileStatus file, long start, long len) throws IOException {
-    if (file == null) {
-      return null;
+  private void setStorageId(BlockLocation bl) {
+    if (setStorageIds != null) {
+      try {
+        setStorageIds.invoke(bl, (Object) getStorageIds());
+      } catch (IllegalAccessException | InvocationTargetException e) {
+        throw new RuntimeException(e);
+      }
     }
+  }
 
+  @Override
+  public BlockLocation[] getFileBlockLocations(FileStatus file, long start, long len) throws IOException {
     if (needCheckPermission() && !checkPathAccess(file.getPath(), FsAction.READ, "getFileBlockLocations")) {
       return superGroupFileSystem.getFileBlockLocations(file, start, len);
     }
 
+    if (isEmpty(discoverNodesUrl) || cacheReplica <= 0) {
+      BlockLocation[] bls = super.getFileBlockLocations(file, start, len);
+      if (bls != null) {
+        for (BlockLocation bl : bls) {
+          setStorageId(bl);
+        }
+      }
+      return bls;
+    }
+
+    if (file == null) {
+      return null;
+    }
+
     if (start < 0 || len < 0) {
       throw new IllegalArgumentException("Invalid start or len parameter");
     }
@@ -901,11 +919,7 @@ public class JuiceFileSystemImpl extends FileSystem {
       String[] host = new String[]{"localhost"};
       return new BlockLocation[]{new BlockLocation(name, host, 0L, file.getLen())};
     }
-
-    BgTaskUtil.putTask(name, "Node fetcher", () -> {
-      initCache(getConf());
-    }, 10, 10, TimeUnit.MINUTES);
-
+    BgTaskUtil.putTask(name, "Node fetcher", this::initCache, 10, 10, TimeUnit.MINUTES);
     if (file.getLen() <= start + len) {
       len = file.getLen() - start;
     }
diff --git a/sdk/java/src/main/java/io/juicefs/utils/FsNodesFetcher.java b/sdk/java/src/main/java/io/juicefs/utils/FsNodesFetcher.java
index c359a432..88e89b13 100644
--- a/sdk/java/src/main/java/io/juicefs/utils/FsNodesFetcher.java
+++ b/sdk/java/src/main/java/io/juicefs/utils/FsNodesFetcher.java
@@ -35,50 +35,25 @@ import java.util.stream.Collectors;
 public class FsNodesFetcher extends NodesFetcher {
   private static final Logger LOG = LoggerFactory.getLogger(FsNodesFetcher.class);
 
-  private FileSystem jfs;
   private Configuration conf;
 
-  public FsNodesFetcher(String jfsName, FileSystem jfs) {
+  public FsNodesFetcher(String jfsName) {
     super(jfsName);
-    this.jfs = jfs;
-    this.conf = jfs.getConf();
+  }
+
+  public void setConf(Configuration conf) {
+    this.conf = conf;
   }
 
   @Override
   public List<String> fetchNodes(String uri) {
-    FileSystem fs;
-
     Path path = new Path(uri);
-    URI toUri = path.toUri();
-    URI defaultUri = FileSystem.getDefaultUri(conf);
-    String scheme = toUri.getScheme();
-    if (scheme == null) {
-      scheme = defaultUri.getScheme();
-    }
-    String authority = toUri.getAuthority();
-    if (authority == null) {
-      authority = defaultUri.getAuthority();
-    }
-    if (scheme != null) {
-      try {
-        String classStr = conf.getTrimmed("fs." + scheme + ".impl");
-        if (JuiceFileSystem.class.getName().equals(classStr) && Objects.equals(authority, jfs.getUri().getAuthority())) {
-          fs = jfs;
-        } else {
-          fs = path.getFileSystem(conf);
-        }
-        if (!fs.exists(path)) {
-          return null;
-        }
-        FSDataInputStream inputStream = fs.open(path);
-        List<String> res = new BufferedReader(new InputStreamReader(inputStream))
-            .lines().collect(Collectors.toList());
-        inputStream.close();
-        return res;
-      } catch (Exception e) {
-        LOG.warn("fetch nodes from {} failed", uri, e);
-        return null;
-      }
+    try (FileSystem fs = FileSystem.newInstance(path.toUri(), conf);
+         FSDataInputStream inputStream = fs.open(path)) {
+      return new BufferedReader(new InputStreamReader(inputStream))
+          .lines().filter(l->!l.isEmpty()).collect(Collectors.toList());
+    } catch (Exception e) {
+      LOG.warn("fetch nodes from {} failed", uri, e);
     }
     return null;
   }
diff --git a/sdk/java/src/main/java/io/juicefs/utils/NodesFetcherBuilder.java b/sdk/java/src/main/java/io/juicefs/utils/NodesFetcherBuilder.java
index 90a3b195..7a5feefa 100644
--- a/sdk/java/src/main/java/io/juicefs/utils/NodesFetcherBuilder.java
+++ b/sdk/java/src/main/java/io/juicefs/utils/NodesFetcherBuilder.java
@@ -16,10 +16,10 @@
 
 package io.juicefs.utils;
 
-import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.conf.Configuration;
 
 public class NodesFetcherBuilder {
-  public static NodesFetcher buildFetcher(String urls, String jfsName, FileSystem jfs) {
+  public static NodesFetcher buildFetcher(String urls, String jfsName, Configuration conf) {
     NodesFetcher fetcher;
     if ((urls.startsWith("http") && urls.contains("cluster/nodes"))
         || "yarn".equals(urls.toLowerCase().trim())) {
@@ -31,7 +31,8 @@ public class NodesFetcherBuilder {
     } else if (urls.startsWith("http") && urls.contains("api/v1/applications")) {
       fetcher = new SparkThriftNodesFetcher(jfsName);
     } else {
-      fetcher = new FsNodesFetcher(jfsName, jfs);
+      fetcher = new FsNodesFetcher(jfsName);
+      ((FsNodesFetcher) fetcher).setConf(conf);
     }
     return fetcher;
   }
diff --git a/sdk/python/juicefs/juicefs/juicefs.py b/sdk/python/juicefs/juicefs/juicefs.py
index 996bfe4e..b76bdf53 100644
--- a/sdk/python/juicefs/juicefs/juicefs.py
+++ b/sdk/python/juicefs/juicefs/juicefs.py
@@ -71,8 +71,11 @@ class JuiceFSLib(object):
 
     def __getattr__(self, n):
         fn = getattr(self.lib, n)
-        if n.startswith("jfs"):
-            fn.restype = c_int
+        if n == "jfs_init" or n == "jfs_lseek":
+            fn.restype = c_int64
+            fn.errcheck = check_error
+        elif n.startswith("jfs"):
+            fn.restype = c_int32
             fn.errcheck = check_error
         return fn
 
@@ -132,6 +135,7 @@ class Client(object):
         kwargs["pushAuth"] = push_auth
         kwargs["pushLabels"] = push_labels
         kwargs["pushGraphite"] = push_graphite
+        kwargs["caller"] = 1
 
         jsonConf = json.dumps(kwargs)
         self.umask = os.umask(0)
@@ -145,7 +149,7 @@ class Client(object):
     def stat(self, path):
         """Get the status of a file or a directory."""
         fi = FileInfo()
-        self.lib.jfs_stat(_tid(), self.h, _bin(path), byref(fi))
+        self.lib.jfs_stat(c_int64(_tid()), c_int64(self.h), _bin(path), byref(fi))
         return os.stat_result((fi.mode, fi.inode, 0, fi.nlink, fi.uid, fi.gid, fi.length, fi.atime, fi.mtime, fi.ctime))
     
     def exists(self, path):
@@ -154,9 +158,7 @@ class Client(object):
             self.stat(path)
             return True
         except OSError as e:
-            if e.errno == errno.ENOENT:
-                return False
-            raise
+            return False
 
     def open(self, path, mode='r', buffering=-1, encoding=None, errors=None):
         """Open a file, returns a filelike object."""
@@ -193,13 +195,13 @@ class Client(object):
 
         size = 0
         if 'x' in mode:
-            fd = self.lib.jfs_create(_tid(), self.h, _bin(path), c_uint16(0o666), c_uint16(self.umask))
+            fd = self.lib.jfs_create(c_int64(_tid()), c_int64(self.h), _bin(path), c_uint16(0o666), c_uint16(self.umask))
         else:
             try:
                 sz = c_uint64()
-                fd = self.lib.jfs_open(_tid(), self.h, _bin(path), byref(sz), c_uint32(flag))
+                fd = self.lib.jfs_open(c_int64(_tid()), c_int64(self.h), _bin(path), byref(sz), c_int32(flag))
                 if 'w' in mode:
-                    self.lib.jfs_ftruncate(_tid(), fd, 0)
+                    self.lib.jfs_ftruncate(c_int64(_tid()), fd, c_uint64(0))
                 else:
                     size = sz.value
             except OSError as e:
@@ -207,33 +209,32 @@ class Client(object):
                     raise e
                 if 'r' in mode:
                     raise FileNotFoundError(e)
-                fd = self.lib.jfs_create(_tid(), self.h, _bin(path), c_uint16(0o666), c_uint16(self.umask))
+                fd = self.lib.jfs_create(c_int64(_tid()), c_int64(self.h), _bin(path), c_uint16(0o666), c_uint16(self.umask))
         return File(self.lib, fd, path, mode, flag, size, buffering, encoding, errors)
 
     def truncate(self, path, size):
         """Truncate a file to a specified size."""
-        self.lib.jfs_truncate(_tid(), self.h, _bin(path), c_uint64(size))
+        self.lib.jfs_truncate(c_int64(_tid()), c_int64(self.h), _bin(path), c_uint64(size))
 
     def remove(self, path):
         """Remove a file."""
-        self.lib.jfs_delete(_tid(), self.h, _bin(path))
+        self.lib.jfs_delete(c_int64(_tid()), c_int64(self.h), _bin(path))
 
     def mkdir(self, path, mode=0o777):
         """Create a directory."""
-        self.lib.jfs_mkdir(_tid(), self.h, _bin(path), c_uint16(mode&0o777), c_uint16(self.umask))
+        self.lib.jfs_mkdir(c_int64(_tid()), c_int64(self.h), _bin(path), c_uint16(mode&0o777), c_uint16(self.umask))
 
-    def makedirs(self, path, mode=0o777):
+    def makedirs(self, path, mode=0o777, exist_ok=False):
         """Create a directory and all its parent components if they do not exist."""
-        print("makedirs: ", path, "--: ", mode, "--: ", self.umask)
-        self.lib.jfs_mkdirAll(_tid(), self.h, _bin(path), c_uint16(mode&0o777), c_uint16(self.umask))
+        self.lib.jfs_mkdirAll(c_int64(_tid()), c_int64(self.h), _bin(path), c_uint16(mode&0o777), c_uint16(self.umask), c_bool(exist_ok))
 
     def rmdir(self, path):
         """Remove a directory. The directory must be empty."""
-        self.lib.jfs_rmr(_tid(), self.h, _bin(path))
+        self.lib.jfs_rmr(c_int64(_tid()), c_int64(self.h), _bin(path))
 
     def rename(self, old, new):
         """Rename the file or directory old to new."""
-        self.lib.jfs_rename(_tid(), self.h, _bin(old), _bin(new), c_uint32(0))
+        self.lib.jfs_rename(c_int64(_tid()), c_int64(self.h), _bin(old), _bin(new), c_uint32(0))
 
     def listdir(self, path, detail=False):
         """Return a list containing the names of the entries in the directory given by path."""
@@ -241,7 +242,7 @@ class Client(object):
         size = c_int()
         # func jfs_listdir(pid int, h int64, cpath *C.char, offset int, buf uintptr, bufsize int) int {
 
-        self.lib.jfs_listdir2(_tid(), self.h, _bin(path), bool(detail), byref(buf), byref(size))
+        self.lib.jfs_listdir2(c_int64(_tid()), c_int64(self.h), _bin(path), bool(detail), byref(buf), byref(size))
         data = string_at(buf, size)
         infos = []
         pos = 0
@@ -262,31 +263,31 @@ class Client(object):
     
     def chmod(self, path, mode):
         """Change the mode of a file."""
-        self.lib.jfs_chmod(_tid(), self.h, _bin(path), c_uint16(mode))
+        self.lib.jfs_chmod(c_int64(_tid()), c_int64(self.h), _bin(path), c_uint16(mode))
 
     def chown(self, path, uid, gid):
         """Change the owner and group id of a file."""
-        self.lib.jfs_chown(_tid(), self.h, _bin(path), c_uint32(uid), c_uint32(gid))
+        self.lib.jfs_chown(c_int64(_tid()), c_int64(self.h), _bin(path), c_uint32(uid), c_uint32(gid))
 
     def link(self, src, dst):
         """Create a hard link to a file."""
-        self.lib.jfs_link(_tid(), self.h, _bin(src), _bin(dst))
+        self.lib.jfs_link(c_int64(_tid()), c_int64(self.h), _bin(src), _bin(dst))
 
     def lstat(self, path):
         """Like stat(), but do not follow symbolic links."""
         info = FileInfo()
-        self.lib.jfs_lstat(_tid(), self.h, _bin(path), byref(info))
+        self.lib.jfs_lstat(c_int64(_tid()), c_int64(self.h), _bin(path), byref(info))
         return os.stat_result((info.mode, info.inode, 0, info.nlink, info.uid, info.gid, info.length, info.atime, info.mtime, info.ctime))
 
     def readlink(self, path):
         """Return a string representing the path to which the symbolic link points."""
         buf = bytes(1<<16)
-        n = self.lib.jfs_readlink(_tid(), self.h, _bin(path), buf, len(buf))
+        n = self.lib.jfs_readlink(c_int64(_tid()), c_int64(self.h), _bin(path), buf, c_int32(len(buf)))
         return buf[:n].decode()
 
     def symlink(self, src, dst):
         """Create a symbolic link."""
-        self.lib.jfs_symlink(_tid(), self.h, _bin(src), _bin(dst))
+        self.lib.jfs_symlink(c_int64(_tid()), c_int64(self.h), _bin(src), _bin(dst))
 
     def unlink(self, path):
         """Remove a file."""
@@ -294,14 +295,14 @@ class Client(object):
 
     def rmr(self, path):
         """Remove a directory and all its contents recursively."""
-        self.lib.jfs_rmr(_tid(), self.h, _bin(path))
+        self.lib.jfs_rmr(c_int64(_tid()), c_int64(self.h), _bin(path))
 
     def utime(self, path, times=None):
         """Set the access and modified times of a file."""
         if not times:
             now = time.time()
             times = (now, now)
-        self.lib.jfs_utime(_tid(), self.h, _bin(path), c_int64(int(times[1]*1000)), c_int64(int(times[0]*1000)))
+        self.lib.jfs_utime(c_int64(_tid()), c_int64(self.h), _bin(path), c_int64(int(times[1]*1000)), c_int64(int(times[0]*1000)))
 
     def walk(self, top, topdown=True, onerror=None, followlinks=False):
         raise NotImplementedError
@@ -310,14 +311,14 @@ class Client(object):
         """Get an extended attribute on a file."""
         size = 64 << 10 # XattrSizeMax
         buf = bytes(size)
-        size = self.lib.jfs_getXattr(_tid(), self.h, _bin(path), _bin(name), buf, size)
+        size = self.lib.jfs_getXattr(c_int64(_tid()), c_int64(self.h), _bin(path), _bin(name), buf, c_int32(size))
         return buf[:size]
 
     def listxattr(self, path):
         """List extended attributes on a file."""
         buf = c_void_p()
         size = c_int()
-        self.lib.jfs_listXattr2(_tid(), self.h, _bin(path), byref(buf), byref(size))
+        self.lib.jfs_listXattr2(c_int64(_tid()), c_int64(self.h), _bin(path), byref(buf), byref(size))
         data = string_at(buf, size).decode()
         self.lib.free(buf)
         if not data:
@@ -327,15 +328,15 @@ class Client(object):
     def setxattr(self, path, name, value, flags=0):
         """Set an extended attribute on a file."""
         value = _bin(value)
-        self.lib.jfs_setXattr(_tid(), self.h, _bin(path), _bin(name), value, len(value), c_int(flags))
+        self.lib.jfs_setXattr(c_int64(_tid()),  c_int64(self.h), _bin(path), _bin(name), value, c_int32(len(value)), c_int32(flags))
 
     def removexattr(self, path, name):
         """Remove an extended attribute from a file."""
-        self.lib.jfs_removeXattr(_tid(), self.h, _bin(path), _bin(name))
+        self.lib.jfs_removeXattr(c_int64(_tid()), c_int64(self.h), _bin(path), _bin(name))
 
     def clone(self, src, dst):
         """Clone a file."""
-        self.lib.jfs_clone(_tid(), self.h, _bin(src), _bin(dst))
+        self.lib.jfs_clone(c_int64(_tid()), c_int64(self.h), _bin(src), _bin(dst))
 
     # def summary(self, path, depth=0, entries=1):
     #     """Get the summary of a directory."""
@@ -400,7 +401,7 @@ class File(object):
         if (not self._readbuf or self._readbuf_off == len(self._readbuf)) and size < self._buffering:
             if not self._readbuf or len(self._readbuf) < self._buffering:
                 self._readbuf = bytes(self._buffering)
-            n = self.lib.jfs_pread(_tid(), self.fd, self._readbuf, self._buffering, c_int64(self.off))
+            n = self.lib.jfs_pread(c_int64(_tid()), c_int32(self.fd), self._readbuf, c_int32(self._buffering), c_int64(self.off))
             if n < self._buffering:
                 self._readbuf = self._readbuf[:n]
             self._readbuf_off = 0
@@ -420,7 +421,7 @@ class File(object):
             while size > 0:
                 n = min(size, 4 << 20)
                 buf = bytes(n)
-                n = self.lib.jfs_pread(_tid(), self.fd, buf, c_uint64(n), c_int64(self.off+got))
+                n = self.lib.jfs_pread(c_int64(_tid()), c_int32(self.fd), buf, c_int32(n), c_int64(self.off+got))
                 if n == 0:
                     break
                 if n < len(buf):
@@ -431,7 +432,7 @@ class File(object):
         elif size < 0:
             while True:
                 buf = bytes(128 << 10)
-                n = self.lib.jfs_pread(_tid(), self.fd, buf, len(buf), c_int64(self.off+got))
+                n = self.lib.jfs_pread(c_int64(_tid()), c_int32(self.fd), buf, c_int32(len(buf)), c_int64(self.off+got))
                 if n == 0:
                     break
                 if n < len(buf):
@@ -479,7 +480,7 @@ class File(object):
             if len(data) < self._buffering:
                 self._writebuf.append(data)
             else:
-                self.lib.jfs_pwrite(_tid(), self.fd, data, c_uint64(len(data)), c_int64(self.off))
+                self.lib.jfs_pwrite(c_int64(_tid()), c_int32(self.fd), data, c_int32(len(data)), c_int64(self.off))
         else:
             self._writebuf.append(data)
         self.off += len(data)
@@ -527,7 +528,7 @@ class File(object):
         self.flush()
         if size is None:
             size = self.tell()
-        self.lib.jfs_ftruncate(_tid(), self.fd, c_uint64(size))
+        self.lib.jfs_ftruncate(c_int64(_tid()), c_int32(self.fd), c_uint64(size))
         self.length = size
         return size
 
@@ -536,20 +537,20 @@ class File(object):
         This does nothing for read-only and non-blocking streams."""
         if self._writebuf:
             data = b''.join(self._writebuf)
-            self.lib.jfs_pwrite(_tid(), self.fd, data, len(data), c_uint64(self.off-len(data)))
+            self.lib.jfs_pwrite(c_int64(_tid()), c_int32(self.fd), data, c_int32(len(data)), c_int64(self.off-len(data)))
             self._writebuf = []
 
     def fsync(self):
         """Force write file data to the backend storage."""
         self.flush()
-        self.lib.jfs_fsync(_tid(), self.fd)
+        self.lib.jfs_fsync(c_int64(_tid()), c_int32(self.fd))
 
     def close(self):
         """Close the file. A closed file cannot be used for further I/O operations."""
         if self.closed:
             return
         self.flush()
-        self.lib.jfs_close(_tid(), self.fd)
+        self.lib.jfs_close(c_int64(_tid()), c_int32(self.fd))
         self.closed = True
 
     def __del__(self):


Test patch:
diff --git a/.github/scripts/hypo/fs_sdk_test.py b/.github/scripts/hypo/fs_sdk_test.py
index d10b8f4b..e2eb5604 100644
--- a/.github/scripts/hypo/fs_sdk_test.py
+++ b/.github/scripts/hypo/fs_sdk_test.py
@@ -132,14 +132,14 @@ class TestPySdk(unittest.TestCase):
         state.readline(file=v2, mode='r', offset=1708,  user='root', whence=0)
         state.teardown()
 
-    def test_issue_1425(self):
-        # SEE: https://github.com/juicedata/jfs/issues/1425
-        state = JuicefsMachine()
-        v1 = state.init_folders()
-        v2 = state.create_file(content=b'a', file_name='a', parent=v1, umask=18,  user='root')
-        v3 = state.mkdir(mode=0, parent=v1, subdir='b', umask=18,  user='root')
-        state.rename_dir(entry=v3, new_entry_name=v2, parent=v1, umask=18,  user='root')
-        state.teardown()
+    # def test_issue_1425(self):
+    #     # SEE: https://github.com/juicedata/jfs/issues/1425
+    #     state = JuicefsMachine()
+    #     v1 = state.init_folders()
+    #     v2 = state.create_file(content=b'a', file_name='a', parent=v1, umask=18,  user='root')
+    #     v3 = state.mkdir(mode=0, parent=v1, subdir='b', umask=18,  user='root')
+    #     state.rename_dir(entry=v3, new_entry_name=v2, parent=v1, umask=18,  user='root')
+    #     state.teardown()
 
     def test_issue_1442(self):
         # SEE: https://github.com/juicedata/jfs/issues/1442
@@ -149,14 +149,14 @@ class TestPySdk(unittest.TestCase):
         state.set_xattr(file=v2, flag=0, name='user.0', user='root', value=b'\x01\x01\x00\x01')
         state.teardown()
 
-    def test_issue_1443(self):
-        # SEE: https://github.com/juicedata/jfs/issues/1443
-        state = JuicefsMachine()
-        v1 = state.init_folders()
-        v2 = state.create_file(content=b'bcb', file_name='bcba', parent=v1, umask=18, user='root')
-        v3 = state.hardlink(src_file=v2, link_file_name='a', parent=v1, umask=18, user='root')
-        state.rename_file(entry=v2, new_entry_name=v3, parent=v1, umask=18, user='root')
-        state.teardown()
+    # def test_issue_1443(self):
+    #     # SEE: https://github.com/juicedata/jfs/issues/1443
+    #     state = JuicefsMachine()
+    #     v1 = state.init_folders()
+    #     v2 = state.create_file(content=b'bcb', file_name='bcba', parent=v1, umask=18, user='root')
+    #     v3 = state.hardlink(src_file=v2, link_file_name='a', parent=v1, umask=18, user='root')
+    #     state.rename_file(entry=v2, new_entry_name=v3, parent=v1, umask=18, user='root')
+    #     state.teardown()
 
     def test_issue_1449(self):
         # SEE: https://github.com/juicedata/jfs/issues/1449
@@ -269,5 +269,13 @@ class TestPySdk(unittest.TestCase):
         state.read(file=v2, mode='r', offset=0, user='root', whence=0, length=4)
         state.teardown()
 
+    def test_issue_f(self):
+        state = JuicefsMachine()
+        folders_0 = state.init_folders()
+        files_0 = state.create_file(content=b'', file_name='b', parent=folders_0, umask=18, user='root')
+        state.chown(entry=folders_0, owner='user1', user='root')
+        state.create_file(content=b'', file_name='a', parent=folders_0, umask=18, user='root')
+        state.teardown()
+
 if __name__ == '__main__':
-    unittest.main()
\ No newline at end of file
+    unittest.main()
diff --git a/.github/scripts/pysdk/pysdk_test.py b/.github/scripts/pysdk/pysdk_test.py
index 9562a531..d59b31cb 100644
--- a/.github/scripts/pysdk/pysdk_test.py
+++ b/.github/scripts/pysdk/pysdk_test.py
@@ -156,6 +156,7 @@ class LinkTests(unittest.TestCase):
     def test_link(self):
         self._test_link(self.file1, self.file2)
 
+@unittest.skip("Skipping SummaryTests")
 class SummaryTests(unittest.TestCase):
     # /test/dir1/file
     #      /dir2
diff --git a/.github/workflows/integrationtests.yml b/.github/workflows/integrationtests.yml
index 0f75b1d6..be235a4c 100644
--- a/.github/workflows/integrationtests.yml
+++ b/.github/workflows/integrationtests.yml
@@ -159,6 +159,7 @@ jobs:
 
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
diff --git a/.github/workflows/pjdfstest.yml b/.github/workflows/pjdfstest.yml
index 67c20b70..caddf8c3 100644
--- a/.github/workflows/pjdfstest.yml
+++ b/.github/workflows/pjdfstest.yml
@@ -139,6 +139,7 @@ jobs:
 
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }}
diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml
index 4f958512..74288318 100644
--- a/.github/workflows/unittests.yml
+++ b/.github/workflows/unittests.yml
@@ -114,6 +114,7 @@ jobs:
 
       - name: upload coverage report
         timeout-minutes: 5
+        continue-on-error: true
         uses: ./.github/actions/upload-coverage
         with:
           UPLOAD_TOKEN: ${{ secrets.CI_COVERAGE_FILE_UPLOAD_AUTH_TOKEN }} 
diff --git a/pkg/fs/fs_test.go b/pkg/fs/fs_test.go
index 47bede03..d1d91cbe 100644
--- a/pkg/fs/fs_test.go
+++ b/pkg/fs/fs_test.go
@@ -230,7 +230,7 @@ func TestFileSystem(t *testing.T) {
 	if err := fs.Delete(ctx, "/d/f"); err == 0 || !IsNotExist(err) {
 		t.Fatalf("delete /d/f: %s", err)
 	}
-	if e := fs.Rmr(ctx, "/d"); e != 0 {
+	if e := fs.Rmr(ctx, "/d", meta.RmrDefaultThreads); e != 0 {
 		t.Fatalf("delete /d -r: %s", e)
 	}
 
@@ -261,7 +261,7 @@ func TestFileSystem(t *testing.T) {
 	if err := fs.Rename(ctx, "/ddd/", "/ttt/", 0); err != 0 {
 		t.Fatalf("delete /ddd/: %s", err)
 	}
-	if err := fs.Rmr(ctx, "/ttt/"); err != 0 {
+	if err := fs.Rmr(ctx, "/ttt/", meta.RmrDefaultThreads); err != 0 {
 		t.Fatalf("rmr /ttt/: %s", err)
 	}
 	if _, err := fs.Stat(ctx, "/ttt/"); err != syscall.ENOENT {
diff --git a/pkg/meta/load_dump_test.go b/pkg/meta/load_dump_test.go
index cd28a235..e19f9e55 100644
--- a/pkg/meta/load_dump_test.go
+++ b/pkg/meta/load_dump_test.go
@@ -92,6 +92,31 @@ func GbkToUtf8(s []byte) ([]byte, error) {
 }
 
 func checkMeta(t *testing.T, m Meta) {
+	if _, err := m.Load(true); err != nil {
+		t.Fatalf("load setting: %s", err)
+	}
+
+	counters := map[string]int64{
+		"usedSpace":   115392512,
+		"totalInodes": 14,
+		"nextInode":   35,
+		"nextChunk":   9,
+		"nextSession": 0,
+		"nextTrash":   1,
+	}
+	for name, expect := range counters {
+		val, err := m.getBase().en.getCounter(name)
+		if err != nil {
+			t.Fatalf("get counter %s: %s", name, err)
+		}
+		if m.Name() == "redis" && (name == "nextChunk" || name == "nextInode") {
+			expect--
+		}
+		if val != expect {
+			t.Fatalf("counter %s: %d != %d", name, val, expect)
+		}
+	}
+
 	ctx := Background()
 	var entries []*Entry
 	if st := m.Readdir(ctx, 1, 1, &entries); st != 0 {
@@ -241,23 +266,6 @@ func checkMeta(t *testing.T, m Meta) {
 	}
 }
 
-func testLoad(t *testing.T, uri, fname string) Meta {
-	m := NewClient(uri, nil)
-	if err := m.Reset(); err != nil {
-		t.Fatalf("reset meta: %s", err)
-	}
-	fp, err := os.Open(fname)
-	if err != nil {
-		t.Fatalf("open file: %s", fname)
-	}
-	defer fp.Close()
-	if err = m.LoadMeta(fp); err != nil {
-		t.Fatalf("load meta: %s", err)
-	}
-	checkMeta(t, m)
-	return m
-}
-
 func testLoadSub(t *testing.T, uri, fname string) {
 	m := NewClient(uri, nil)
 	if err := m.Reset(); err != nil {
@@ -314,7 +322,7 @@ func testDump(t *testing.T, m Meta, root Ino, expect, result string) {
 
 func testLoadDump(t *testing.T, name, addr string) {
 	t.Run("Metadata Engine: "+name, func(t *testing.T) {
-		m := testLoad(t, addr, sampleFile)
+		m := testLoad(t, addr, sampleFile, false)
 		testDump(t, m, 1, sampleFile, "test.dump")
 		m.Shutdown()
 		conf := DefaultConf()
@@ -330,7 +338,7 @@ func testLoadDump(t *testing.T, name, addr string) {
 
 func TestLoadDump(t *testing.T) { //skip mutate
 	testLoadDump(t, "redis", "redis://127.0.0.1/10")
-	testLoadDump(t, "mysql", "mysql://root:@/dev")
+	// testLoadDump(t, "mysql", "mysql://root:@/dev")
 	testLoadDump(t, "badger", "badger://jfs-load-dump")
 	testLoadDump(t, "tikv", "tikv://127.0.0.1:2379/jfs-load-dump")
 }
@@ -353,7 +361,7 @@ func testDumpV2(t *testing.T, m Meta, result string, opt *DumpOption) {
 	fp.Sync()
 }
 
-func testLoadV2(t *testing.T, uri, fname string) Meta {
+func testLoad(t *testing.T, uri, fname string, v2 bool) Meta {
 	m := NewClient(uri, nil)
 	if err := m.Reset(); err != nil {
 		t.Fatalf("reset meta: %s", err)
@@ -363,11 +371,14 @@ func testLoadV2(t *testing.T, uri, fname string) Meta {
 		t.Fatalf("open file: %s", fname)
 	}
 	defer fp.Close()
-	if err = m.LoadMetaV2(Background(), fp, &LoadOption{Threads: 10}); err != nil {
-		t.Fatalf("load meta: %s", err)
-	}
-	if _, err := m.Load(true); err != nil {
-		t.Fatalf("load setting: %s", err)
+	if v2 {
+		if err = m.LoadMetaV2(Background(), fp, &LoadOption{Threads: 10}); err != nil {
+			t.Fatalf("load meta: %s", err)
+		}
+	} else {
+		if err = m.LoadMeta(fp); err != nil {
+			t.Fatalf("load meta: %s", err)
+		}
 	}
 	checkMeta(t, m)
 	return m
@@ -376,14 +387,14 @@ func testLoadV2(t *testing.T, uri, fname string) Meta {
 func testLoadDumpV2(t *testing.T, name, addr1, addr2 string) {
 	t.Run("Metadata Engine: "+name, func(t *testing.T) {
 		start := time.Now()
-		m := testLoad(t, addr1, sampleFile)
+		m := testLoad(t, addr1, sampleFile, false)
 		t.Logf("load meta: %v", time.Since(start))
 		start = time.Now()
 		testDumpV2(t, m, fmt.Sprintf("%s.dump", name), nil)
 		m.Shutdown()
 		t.Logf("dump meta v2: %v", time.Since(start))
 		start = time.Now()
-		m = testLoadV2(t, addr2, fmt.Sprintf("%s.dump", name))
+		m = testLoad(t, addr2, fmt.Sprintf("%s.dump", name), true)
 		m.Shutdown()
 		t.Logf("load meta v2: %v", time.Since(start))
 	})
@@ -391,7 +402,7 @@ func testLoadDumpV2(t *testing.T, name, addr1, addr2 string) {
 
 func testLoadOtherEngine(t *testing.T, src, dst, dstAddr string) {
 	t.Run(fmt.Sprintf("Load %s to %s", src, dst), func(t *testing.T) {
-		m := testLoadV2(t, dstAddr, fmt.Sprintf("%s.dump", src))
+		m := testLoad(t, dstAddr, fmt.Sprintf("%s.dump", src), true)
 		m.Shutdown()
 	})
 }
@@ -436,12 +447,12 @@ func TestLoadDumpSlow(t *testing.T) { //skip mutate
 func TestLoadDump_MemKV(t *testing.T) {
 	t.Run("Metadata Engine: memkv", func(t *testing.T) {
 		_ = os.Remove(settingPath)
-		m := testLoad(t, "memkv://test/jfs", sampleFile)
+		m := testLoad(t, "memkv://test/jfs", sampleFile, false)
 		testDump(t, m, 1, sampleFile, "test.dump")
 	})
 	t.Run("Metadata Engine: memkv; --SubDir d1 ", func(t *testing.T) {
 		_ = os.Remove(settingPath)
-		m := testLoad(t, "memkv://user:pass@test/jfs", sampleFile)
+		m := testLoad(t, "memkv://user:pass@test/jfs", sampleFile, false)
 		if kvm, ok := m.(*kvMeta); ok { // memkv will be empty if created again
 			if st := kvm.Chroot(Background(), "d1"); st != 0 {
 				t.Fatalf("Chroot to subdir d1: %s", st)
@@ -455,16 +466,16 @@ func TestLoadDump_MemKV(t *testing.T) {
 }
 
 func testSecretAndTrash(t *testing.T, addr, addr2 string) {
-	m := testLoad(t, addr, sampleFile)
+	m := testLoad(t, addr, sampleFile, false)
 	testDumpV2(t, m, "sqlite-secret.dump", &DumpOption{Threads: 10, KeepSecret: true})
-	m2 := testLoadV2(t, addr2, "sqlite-secret.dump")
+	m2 := testLoad(t, addr2, "sqlite-secret.dump", true)
 	if m2.GetFormat().EncryptKey != m.GetFormat().EncryptKey {
 		t.Fatalf("encrypt key not valid: %s", m2.GetFormat().EncryptKey)
 	}
 	testDumpV2(t, m, "sqlite-non-secret.dump", &DumpOption{Threads: 10, KeepSecret: false})
 	m2.Shutdown()
 
-	m2 = testLoadV2(t, addr2, "sqlite-non-secret.dump")
+	m2 = testLoad(t, addr2, "sqlite-non-secret.dump", true)
 	if m2.GetFormat().EncryptKey != "removed" {
 		t.Fatalf("encrypt key not valid: %s", m2.GetFormat().EncryptKey)
 	}