From d23762a7e926f896a3dd615677882b8e98ed29df Mon Sep 17 00:00:00 2001 From: Davies Liu Date: Fri, 8 Jan 2021 17:44:26 +0800 Subject: [PATCH] first public release --- .github/workflows/release.yml | 27 + .gitignore | 16 + .goreleaser.yml | 38 + .travis.yml | 28 + CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md | 35 + LICENSE | 661 +++++++ Makefile | 34 + README.md | 183 ++ cmd/format.go | 238 +++ cmd/main.go | 70 + cmd/mount.go | 283 +++ cmd/usage.go | 88 + cmd/version.go | 28 + docs/fio.md | 68 + docs/images/juicefs-arch.png | Bin 0 -> 172120 bytes docs/images/juicefs-logo.png | Bin 0 -> 20856 bytes docs/images/juicefs-storage-format.png | Bin 0 -> 113469 bytes docs/images/metadata-benchmark.svg | 1 + .../sequential-read-write-benchmark.svg | 1 + docs/mdtest.md | 117 ++ fstests/Makefile | 38 + go.mod | 17 + go.sum | 265 +++ pkg/chunk/cached_store.go | 780 ++++++++ pkg/chunk/cached_store_test.go | 66 + pkg/chunk/chunk.go | 40 + pkg/chunk/disk_cache.go | 601 +++++++ pkg/chunk/disk_cache_test.go | 69 + pkg/chunk/disk_store.go | 111 ++ pkg/chunk/mem_cache.go | 122 ++ pkg/chunk/page.go | 133 ++ pkg/chunk/page_test.go | 80 + pkg/chunk/prefetch.go | 60 + pkg/chunk/singleflight.go | 69 + pkg/chunk/singleflight_test.go | 40 + pkg/chunk/store_test.go | 110 ++ pkg/chunk/utils_darwin.go | 30 + pkg/chunk/utils_linux.go | 30 + pkg/chunk/utils_unix.go | 47 + pkg/fuse/context.go | 88 + pkg/fuse/fuse.go | 455 +++++ pkg/fuse/fuse_darwin.go | 27 + pkg/fuse/fuse_linux.go | 28 + pkg/fuse/utils.go | 54 + pkg/meta/config.go | 34 + pkg/meta/context.go | 36 + pkg/meta/interface.go | 143 ++ pkg/object/redis.go | 91 + pkg/object/redis_test.go | 40 + pkg/redis/redis.go | 1585 +++++++++++++++++ pkg/redis/redis_test.go | 214 +++ pkg/redis/slice.go | 77 + pkg/utils/alloc.go | 65 + pkg/utils/buffer.go | 144 ++ pkg/utils/buffer_test.go | 71 + pkg/utils/compress.go | 102 ++ pkg/utils/compress_test.go | 125 ++ pkg/utils/cond.go | 76 + pkg/utils/logger.go | 108 ++ pkg/utils/logger_syslog.go | 72 + pkg/utils/utils.go | 51 + pkg/vfs/accesslog.go | 100 ++ pkg/vfs/handle.go | 226 +++ pkg/vfs/helpers.go | 102 ++ pkg/vfs/internal.go | 90 + pkg/vfs/reader.go | 831 +++++++++ pkg/vfs/vfs.go | 899 ++++++++++ pkg/vfs/vfs_unix.go | 281 +++ pkg/vfs/writer.go | 485 +++++ 70 files changed, 11324 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 .travis.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/format.go create mode 100644 cmd/main.go create mode 100644 cmd/mount.go create mode 100644 cmd/usage.go create mode 100644 cmd/version.go create mode 100644 docs/fio.md create mode 100644 docs/images/juicefs-arch.png create mode 100644 docs/images/juicefs-logo.png create mode 100644 docs/images/juicefs-storage-format.png create mode 100644 docs/images/metadata-benchmark.svg create mode 100644 docs/images/sequential-read-write-benchmark.svg create mode 100644 docs/mdtest.md create mode 100644 fstests/Makefile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/chunk/cached_store.go create mode 100644 pkg/chunk/cached_store_test.go create mode 100644 pkg/chunk/chunk.go create mode 100644 pkg/chunk/disk_cache.go create mode 100644 pkg/chunk/disk_cache_test.go create mode 100644 pkg/chunk/disk_store.go create mode 100644 pkg/chunk/mem_cache.go create mode 100644 pkg/chunk/page.go create mode 100644 pkg/chunk/page_test.go create mode 100644 pkg/chunk/prefetch.go create mode 100644 pkg/chunk/singleflight.go create mode 100644 pkg/chunk/singleflight_test.go create mode 100644 pkg/chunk/store_test.go create mode 100644 pkg/chunk/utils_darwin.go create mode 100644 pkg/chunk/utils_linux.go create mode 100644 pkg/chunk/utils_unix.go create mode 100644 pkg/fuse/context.go create mode 100644 pkg/fuse/fuse.go create mode 100644 pkg/fuse/fuse_darwin.go create mode 100644 pkg/fuse/fuse_linux.go create mode 100644 pkg/fuse/utils.go create mode 100644 pkg/meta/config.go create mode 100644 pkg/meta/context.go create mode 100644 pkg/meta/interface.go create mode 100644 pkg/object/redis.go create mode 100644 pkg/object/redis_test.go create mode 100644 pkg/redis/redis.go create mode 100644 pkg/redis/redis_test.go create mode 100644 pkg/redis/slice.go create mode 100644 pkg/utils/alloc.go create mode 100644 pkg/utils/buffer.go create mode 100644 pkg/utils/buffer_test.go create mode 100644 pkg/utils/compress.go create mode 100644 pkg/utils/compress_test.go create mode 100644 pkg/utils/cond.go create mode 100644 pkg/utils/logger.go create mode 100644 pkg/utils/logger_syslog.go create mode 100644 pkg/utils/utils.go create mode 100644 pkg/vfs/accesslog.go create mode 100644 pkg/vfs/handle.go create mode 100644 pkg/vfs/helpers.go create mode 100644 pkg/vfs/internal.go create mode 100644 pkg/vfs/reader.go create mode 100644 pkg/vfs/vfs.go create mode 100644 pkg/vfs/vfs_unix.go create mode 100644 pkg/vfs/writer.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..238492a25463 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: release + +on: + push: + tags: + - v* + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: '1.13.x' + + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: setup release environment + run: |- + echo 'GITHUB_TOKEN=${{secrets.GITHUB_TOKEN}}' > .release-env + + - name: release publish + run: make release diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..90dc1008ae6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.o +*.sw[po] +ltmain.sh +*.orig +*.rej +.deps +.dirstamp +.vscode +.idea +fstests/secfs.test +!fstests/Makefile +jfs +juicefs +dist/ +*.rdb +.release-env diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 000000000000..f6e62cd2d263 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,38 @@ +project_name: juicefs +env: + - GO111MODULE=on + - GOPROXY=https://gocenter.io +before: + hooks: + - go mod download +builds: + - id: juicefs-darwin + env: + - CGO_ENABLED=1 + - CC=o64-clang + - CXX=o64-clang++ + ldflags: -s -w -X main.VERSION={{.Version}} -X main.REVISION={{.ShortCommit}} -X main.REVISIONDATE={{.CommitDate}} + main: ./cmd + goos: + - darwin + goarch: + - amd64 + - id: juicefs-linux + env: + - CGO_ENABLED=1 + ldflags: -s -w -X main.VERSION={{.Version}} -X main.REVISION={{.ShortCommit}} -X main.REVISIONDATE={{.CommitDate}} + main: ./cmd + goos: + - linux + goarch: + - amd64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000000..ad0c74d6e4a5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +os: linux +dist: xenial +language: go +go: + - "1.13" +env: + - DURATION=10 TEST_SUITE=all +addons: + apt: + packages: + - g++-multilib + - libacl1-dev + - redis-server + - attr +install: true +before_script: + - export GO111MODULE=on + - make + - sudo make -C fstests setup +script: + - go test ./pkg/... + - sudo DURATION=${DURATION} make -C fstests ${TEST_SUITE} + - make -C fstests flock +after_failure: + - sudo tail -n 100 /var/log/syslog +notifications: + slack: + secure: VD92Vium23tdNvXyb1cQTI/4/JGXecXY3t2YHjpzfrIWc8RzGxYN28275g6a8gL+AEURDcsLPUxr243tHSqAyQmHhM4JHrX+eXTshqOKcikSzU6kYQTZE6B1vigp1bgn68ExdM9jFMnue4MmsN/tsGsZ5t8eM0lFERdActZaDoR1c5DJ1WMjAUKzUhihGV+10QHI8nZwh+G3zM9SmSfh6obFk3QN0zKCMJEyC8z9UPS3MQspwt23MySYIVHU0beu0PkcZHnXbXNNG3P27e349f8uChhJSwnLVS5G0zFVrxO3rWVTlEAU1ksUeGFPP883v3XwDrFxIT1WOujH7uKwEV/QT3d+8+mmL8yFr1gnU4Gl3hApRzepsvr/IIQjaGtC7HHIqv4gvP/FJB9r5VDH4al4pSbAtMfWYwspmfiPI5mADpmUqW0Anwt9gnTQ5NxPVVo5txyG9LSwseYQ/P2Tx3h8w/kPvW0sPqX/qIxxwnZLVzRbdOGfUlNfmwEmBqWPGkJIEsYto8BEpw932p6ytijElTj+3ISo8hn2L1Lm+vCbsWRWTVyjNL5lqfBCNdsh7JHBUgcBdm1lQ2kKogWtAPtY5DxqLMto9vyVCIoM+gCE1VWZHTjazaFbhLsIg7j2qvwZs97VGQ3Wr/FfqVh3/3mUJNj6X0CWRBpWJRQwVDA= diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000000..049eafc7621c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing to JuiceFS + +## Guidelines + +- Before starting work on a feature or bug fix, please search GitHub or reach out to us via GitHub, Slack etc. The purpose of this step is make sure no one else is already working on it and we'll ask you to open a GitHub issue if necessary. +- We will use the GitHub issue to discuss the feature and come to agreement. This is to prevent your time being wasted, as well as ours. +- If it is a major feature update, we highly recommend you also write a design document to help the community understand your motivation and solution. +- A good way to find a project properly sized for a first time contributor is to search for open issues with the label ["kind/good-first-issue"](https://github.com/juicedata/juicefs/labels/kind%2Fgood-first-issue) or ["kind/help-wanted"](https://github.com/juicedata/juicefs/labels/kind%2Fhelp-wanted). + +## Coding Style + +- We're following ["Effective Go"](https://golang.org/doc/effective_go.html) and ["Go Code Review Comments"](https://github.com/golang/go/wiki/CodeReviewComments). +- Use `go fmt` to format your code before committing. You can find information in editor support for Go tools in ["IDEs and Plugins for Go"](https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins). +- If you see any code which clearly violates the style guide, please fix it and send a pull request. +- Every new source file must begin with a license header. + +## What is a Good PR + +- Presence of unit tests +- Adherence to the coding style +- Adequate in-line comments +- Explanatory commit message + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where to base the contribution. This is usually `main`. +- Make commits of logical units. +- Make sure commit messages are in the proper format. +- Push changes in a topic branch to a personal fork of the repository. +- Submit a pull request to [juicedata/juicefs](https://github.com/juicedata/juicefs/compare). The PR should link to one issue which either created by you or others. +- The PR must receive approval from at least one maintainer before it be merged. + +Happy hacking! diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000000..29ebfa545f55 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 000000000000..f0db4f944c9a --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +export GO111MODULE=on + +all: juicefs + +REVISION := $(shell git rev-parse --short HEAD || unknown) +REVISIONDATE := $(shell git log -1 --pretty=format:'%ad' --date short) +VERSION := $(shell git describe --tags --match 'v*' | sed -e 's/^v//' -e 's/-g[0-9a-f]\{7,\}$$//') +LDFLAGS = -s -w -X main.REVISION=$(REVISION) \ + -X main.REVISIONDATE=$(REVISIONDATE) \ + -X main.VERSION=$(VERSION) +SHELL = /bin/sh + +juicefs: Makefile cmd/*.go pkg/*/*.go + go build -ldflags="$(LDFLAGS)" -o juicefs ./cmd + +.PHONY: snapshot release +snapshot: + docker run --rm --privileged \ + -e PRIVATE_KEY=${PRIVATE_KEY} \ + -v ~/go/pkg/mod:/go/pkg/mod \ + -v `pwd`:/go/src/github.com/juicedata/juicefs \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -w /go/src/github.com/juicedata/juicefs \ + goreng/golang-cross:latest release --snapshot --rm-dist --skip-publish + +release: + docker run --rm --privileged \ + -e PRIVATE_KEY=${PRIVATE_KEY} \ + --env-file .release-env \ + -v ~/go/pkg/mod:/go/pkg/mod \ + -v `pwd`:/go/src/github.com/juicedata/juicefs \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -w /go/src/github.com/juicedata/juicefs \ + goreng/golang-cross:latest release --rm-dist diff --git a/README.md b/README.md new file mode 100644 index 000000000000..8a15d403ee28 --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +

JuiceFS Logo

+

+ Build Status + Go Report Card + Join Slack +

+ +**JuiceFS** is an open-source POSIX file system built on top of [Redis](https://redis.io) and object storage (e.g. Amazon S3), designed and optimized for cloud native environment. By using the widely adopted Redis and S3 as the persistent storage, JuiceFS serves as a stateless middleware to enable many applications to share data easily. + +The highlighted features are: + +- **Fully POSIX-compatible**: JuiceFS is a fully POSIX-compatible file system. Existing applications can work with it without any changes. See [pjdfstest result](#posix-compatibility) below. +- **Strong Consistency**: All confirmed changes made to your data will be reflected in different machines immediately. +- **Outstanding Performance**: The latency can be as low as a few microseconds and the throughput can be expanded to nearly unlimited. See [benchmark result](#performance-benchmark) below. +- **Cloud Native**: By utilize cloud object storage, you could scaling storage and compute independently, a.k.a. disaggregated storage and compute architecture. +- **Sharing**: JuiceFS is a shared file storage can be read and write by many clients. +- **Global File Locks**: JuiceFS supports both BSD locks (flock) and POSIX record locks (fcntl). +- **Data Compression**: By default JuiceFS uses [LZ4](https://lz4.github.io/lz4) to compress all your data, you could also use [Zstandard](https://facebook.github.io/zstd) instead. + +--- + +[Architecture](#architecture) | [Getting Started](#getting-started) | [POSIX Compatibility](#posix-compatibility) | [Performance Benchmark](#performance-benchmark) | [Supported Object Storage](#supported-object-storage) | [Status](#status) | [Roadmap](#roadmap) | [Reporting Issues](#reporting-issues) | [Contributing](#contributing) | [Community](#community) | [Usage Tracking](#usage-tracking) | [License](#license) | [Credits](#credits) | [FAQ](#faq) + +--- + +## Architecture + +![JuiceFS Architecture](docs/images/juicefs-arch.png) + +JuiceFS relies on Redis to store file system metadata. Redis is a fast, open-source, in-memory key-value data store and very suitable for store the metadata. All the data will store into object storage through JuiceFS client. + +![JuiceFS Storage Format](docs/images/juicefs-storage-format.png) + +The storage format of one file in JuiceFS consists of three levels. The first level called **"Chunk"**. Each chunk has fixed size, currently it is 64MiB and cannot be changed. The second level called **"Slice"**. The slice size is variable. A chunk may have multiple slices. The third level called **"Block"**. Like chunk, it's size is fixed. By default one block is 4MiB and you could modify it when format a volume (see following section). At last, the block will be compressed and encrypted (optional) store into object storage. + +## Getting Started + +### Precompiled binaries + +You can download precompiled binaries from [releases page](https://github.com/juicedata/juicefs/releases). + +### Building from source + +You need install [Go](https://golang.org) first, then run following commands: + +```bash +$ git clone git@github.com:juicedata/juicefs.git +$ make +``` + +### Dependency + +A Redis server (>= 2.2) is needed for metadata, please follow [Redis Quick Start](https://redis.io/topics/quickstart). + +[macFUSE](https://osxfuse.github.io/) is also needed for macOS. + +The last one you need is object storage. There are many options for object storage, local disk is the easiest one to get started. + +### Format a volume + +Assume you have a Redis server running locally, we can create a volume called `test` using it to store metadata: + +```bash +$ ./juicefs format localhost test +``` + +It will create a volume with default settings. If there Redis server is not running locally, the address could be specifed using URL, for example, `redis://username:password@host:6379/1`. + +As JuiceFS relies on object storage to store data, you can specify a object storage using `--storage`, `--bucket`, `--accesskey` and `--secretkey`. By default, it uses a local directory to serve as an object store, for all the options, please see `./juicefs format -h`. + +### Mount a volume + +Once a volume is formated, your can mount it to a directory, which is called *mount point*. + +```bash +$ ./juicefs mount -d localhost ~/jfs +``` + +After that you can access the volume just like a local directory. + +To get all options, just run `./juicefs mount -h`. + +## POSIX Compatibility + +JuiceFS passed all of the 8813 tests in latest [pjdfstest](https://github.com/pjd/pjdfstest). + +``` +All tests successful. + +Test Summary Report +------------------- +/root/soft/pjdfstest/tests/chown/00.t (Wstat: 0 Tests: 1323 Failed: 0) + TODO passed: 693, 697, 708-709, 714-715, 729, 733 +Files=235, Tests=8813, 233 wallclock secs ( 2.77 usr 0.38 sys + 2.57 cusr 3.93 csys = 9.65 CPU) +Result: PASS +``` + +## Performance Benchmark + +### Throughput + +Performed a sequential read/write benchmark on JuiceFS, [EFS](https://aws.amazon.com/efs) and [S3FS](https://github.com/s3fs-fuse/s3fs-fuse) by [fio](https://github.com/axboe/fio), here is the result: + +![Sequential Read Write Benchmark](docs/images/sequential-read-write-benchmark.svg) + +It shows JuiceFS can provide 10X more throughput than the other two, read [more details](docs/fio.md). + +### Metadata IOPS + +Performed a simple mdtest benchmark on JuiceFS, [EFS](https://aws.amazon.com/efs) and [S3FS](https://github.com/s3fs-fuse/s3fs-fuse) by [mdtest](https://github.com/hpc/ior), here is the result: + +![Metadata Benchmark](docs/images/metadata-benchmark.svg) + +It shows JuiceFS can provide significantly more metadata IOPS than the other two, read [more details](docs/mdtest.md). + +## Supported Object Storage + +- Amazon S3 +- Google Cloud Storage +- Azure Blob Storage +- Alibaba Cloud Object Storage Service (OSS) +- Tencent Cloud Object Storage (COS) +- Ceph RGW +- MinIO +- Local disk +- Redis + +For the detailed list, see [juicesync](https://github.com/juicedata/juicesync). + +## Status + +It's considered as beta quality, the storage format is not stabilized yet. It's not recommended to deploy it into production environment. Please test it with your use cases and give us feedback. + +## Roadmap + +- Data compaction +- Kubernetes CSI driver +- Stabilize storage format +- Hadoop SDK +- S3 gateway +- Windows client +- Encryption at rest +- Other databases for metadata + +## Reporting Issues + +We use [GitHub Issues](https://github.com/juicedata/juicefs/issues) to track community reported issues. You can also [contact](#community) the community for getting answers. + +## Contributing + +Thank you for your contribution! Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) for more information. + +## Community + +Welcome to join our [Slack channel](https://join.slack.com/t/juicefs/shared_invite/zt-kjbre7de-K8jeTMouDZE8nKEZVHLAMQ) to connect with JuiceFS team members and other users. + +## Usage Tracking + +JuiceFS by default collects **anonymous** usage data. It only collects core metrics (e.g. version number), no user or any sensitive data will be collected. You could review related code [here](cmd/usage.go). + +These data help us understand how the community is using this project. You could disable reporting easily by command line option `--no-usage-report`: + +```bash +$ ./juicefs mount --no-usage-report +``` + +## License + +JuiceFS is open-sourced under GNU AGPL v3.0, see [LICENSE](LICENSE). + +## Credits + +The design of JuiceFS was inspired by [Google File System](https://research.google/pubs/pub51), [HDFS](https://hadoop.apache.org) and [MooseFS](https://moosefs.com), thanks to their great work. + +## FAQ + +### Why doesn't JuiceFS support XXX object storage? + +JuiceFS already supported many object storage, please check [the list](#supported-object-storage) first. If you couldn't found it, try [reporting issue](#reporting-issues) to the community. + +### Can I use Redis cluster? + +The simple answer is no. JuiceFS uses [transaction](https://redis.io/topics/transactions) to guarantee the atomicity of metadata operations, which is not well supported in cluster mode. diff --git a/cmd/format.go b/cmd/format.go new file mode 100644 index 000000000000..a5bb29ff1ce6 --- /dev/null +++ b/cmd/format.go @@ -0,0 +1,238 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "math/rand" + _ "net/http/pprof" + "os" + "path" + "runtime" + "strings" + "time" + + "github.com/google/uuid" + "github.com/juicedata/juicefs/pkg/meta" + "github.com/juicedata/juicefs/pkg/redis" + "github.com/juicedata/juicefs/pkg/utils" + "github.com/juicedata/juicesync/object" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +func fixObjectSize(s int) int { + var bits uint + for s > 1 { + bits++ + s /= 2 + } + s = s << bits + if s < 64 { + s = 64 + } else if s > 16<<10 { + s = 16 << 10 + } + return s +} + +func createStorage(fmt *meta.Format) (object.ObjectStorage, error) { + blob, err := object.CreateStorage(strings.ToLower(fmt.Storage), fmt.Bucket, fmt.AccessKey, fmt.SecretKey) + if err != nil { + return nil, err + } + return object.WithPrefix(blob, fmt.Name+"/") +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + +func randSeq(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +func doTesting(store object.ObjectStorage, key string, data []byte) error { + if err := store.Put(key, bytes.NewReader(data)); err != nil { + if strings.Contains(err.Error(), "Access Denied") { + return fmt.Errorf("Failed to put: %s", err) + } + if err2 := store.Create(); err2 != nil { + return fmt.Errorf("Failed to create %s: %s, previous error: %s\nplease create bucket %s manually, then format again", + store, err2, err, store) + } + if err := store.Put(key, bytes.NewReader(data)); err != nil { + return fmt.Errorf("Failed to put: %s", err) + } + } + p, err := store.Get(key, 0, -1) + if err != nil { + return fmt.Errorf("Failed to get: %s", err) + } + data2, err := ioutil.ReadAll(p) + p.Close() + if !bytes.Equal(data, data2) { + return fmt.Errorf("Read wrong data") + } + err = store.Delete(key) + if err != nil { + fmt.Printf("Failed to delete: %s", err) + } + return nil +} + +func test(store object.ObjectStorage) error { + rand.Seed(int64(time.Now().UnixNano())) + key := "testing/" + randSeq(10) + data := make([]byte, 100) + rand.Read(data) + nRetry := 3 + var err error + for i := 0; i < nRetry; i++ { + err = doTesting(store, key, data) + if err == nil { + return nil + } + time.Sleep(time.Second * time.Duration(i*3+1)) + } + return err +} + +func format(c *cli.Context) error { + if c.Bool("trace") { + utils.SetLogLevel(logrus.TraceLevel) + } else if c.Bool("debug") { + utils.SetLogLevel(logrus.DebugLevel) + } else if c.Bool("quiet") { + utils.SetLogLevel(logrus.ErrorLevel) + utils.InitLoggers(!c.Bool("nosyslog")) + } + + if c.Args().Len() < 1 { + logger.Fatalf("Redis URL and name are required") + } + addr := c.Args().Get(0) + if !strings.Contains(addr, "://") { + addr = "redis://" + addr + } + logger.Infof("Meta address: %s", addr) + var rc = redis.RedisConfig{Retries: 10} + m, err := redis.NewRedisMeta(addr, &rc) + if err != nil { + logger.Fatalf("Meta is not available: %s", err) + } + + if c.Args().Len() < 2 { + logger.Fatalf("Please give it a name") + } + format := meta.Format{ + Name: c.Args().Get(1), + UUID: uuid.New().String(), + Storage: c.String("storage"), + Bucket: c.String("bucket"), + AccessKey: c.String("accesskey"), + SecretKey: c.String("secretkey"), + BlockSize: fixObjectSize(c.Int("blockSize")), + Compression: c.String("compress"), + } + if format.AccessKey == "" && os.Getenv("ACCESS_KEY") != "" { + format.AccessKey = os.Getenv("ACCESS_KEY") + os.Unsetenv("ACCESS_KEY") + } + if format.SecretKey == "" && os.Getenv("SECRET_KEY") != "" { + format.SecretKey = os.Getenv("SECRET_KEY") + os.Unsetenv("SECRET_KEY") + } + + if format.Storage == "file" && !strings.HasSuffix(format.Bucket, "/") { + format.Bucket += "/" + } + + object.UserAgent = "JuiceFS-" + Version() + + blob, err := createStorage(&format) + if err != nil { + logger.Fatalf("object storage: %s", err) + } + logger.Infof("Data uses %s", blob) + if err := test(blob); err != nil { + logger.Fatalf("Storage %s is not configured correctly: %s", blob, err) + return err + } + + err = m.Init(format) + if err != nil { + logger.Fatalf("format: %s", err) + return err + } + if format.SecretKey != "" { + format.SecretKey = "removed" + } + logger.Infof("Volume is formatted as %+v", format) + return nil +} + +func formatFlags() *cli.Command { + var defaultBucket = "/var/jfs" + if runtime.GOOS == "darwin" { + homeDir, err := os.UserHomeDir() + if err != nil { + logger.Fatalf("%v", err) + return nil + } + defaultBucket = path.Join(homeDir, ".juicefs", "local") + } + return &cli.Command{ + Name: "format", + Usage: "format a volume", + ArgsUsage: "REDIS-URL NAME", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "blockSize", + Value: 4096, + Usage: "size of block in KiB", + }, + &cli.StringFlag{ + Name: "compress", + Value: "lz4", + Usage: "compression algorithm", + }, + &cli.StringFlag{ + Name: "storage", + Value: "file", + Usage: "Object storage type (e.g. s3, gcs, oss, cos)", + }, + &cli.StringFlag{ + Name: "bucket", + Value: defaultBucket, + Usage: "A bucket URL to store data", + }, + &cli.StringFlag{ + Name: "accesskey", + Usage: "Access key for object storage", + }, + &cli.StringFlag{ + Name: "secretkey", + Usage: "Secret key for object storage", + }, + }, + Action: format, + } +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 000000000000..9d31cf155b8a --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,70 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "log" + _ "net/http/pprof" + "os" + + _ "github.com/juicedata/juicefs/pkg/object" + "github.com/juicedata/juicefs/pkg/utils" + "github.com/urfave/cli/v2" +) + +var logger = utils.GetLogger("juicefs") + +func main() { + cli.VersionFlag = &cli.BoolFlag{ + Name: "version", Aliases: []string{"V"}, + Usage: "print only the version", + } + app := &cli.App{ + Name: "juicefs", + Usage: "A POSIX filesystem built on redis and object storage.", + Version: Version(), + Copyright: "AGPLv3", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Aliases: []string{"v"}, + Usage: "enable debug log", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "only warning and errors", + }, + &cli.BoolFlag{ + Name: "trace", + Usage: "enable trace log", + }, + &cli.BoolFlag{ + Name: "nosyslog", + Usage: "disable syslog", + }, + }, + Commands: []*cli.Command{ + formatFlags(), + mountFlags(), + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/cmd/mount.go b/cmd/mount.go new file mode 100644 index 000000000000..7d180ca567b4 --- /dev/null +++ b/cmd/mount.go @@ -0,0 +1,283 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "fmt" + "net/http" + _ "net/http/pprof" + "os" + "os/exec" + "os/signal" + "path" + "runtime" + "strings" + "syscall" + "time" + + "github.com/VividCortex/godaemon" + "github.com/google/gops/agent" + "github.com/juicedata/juicefs/pkg/chunk" + "github.com/juicedata/juicefs/pkg/fuse" + "github.com/juicedata/juicefs/pkg/meta" + "github.com/juicedata/juicefs/pkg/redis" + "github.com/juicedata/juicefs/pkg/utils" + "github.com/juicedata/juicefs/pkg/vfs" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +func MakeDaemon() { + godaemon.MakeDaemon(&godaemon.DaemonAttr{}) +} + +func installHandler(mp string) { + // Go will catch all the signals + signal.Ignore(syscall.SIGPIPE) + signalChan := make(chan os.Signal, 10) + signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) + go func() { + for { + <-signalChan + go func() { + if runtime.GOOS == "linux" { + exec.Command("umount", mp, "-l").Run() + } else if runtime.GOOS == "darwin" { + exec.Command("diskutil", "umount", "force", mp).Run() + } + }() + go func() { + time.Sleep(time.Second * 3) + os.Exit(1) + }() + } + }() +} + +func mount(c *cli.Context) error { + go func() { + for port := 6060; port < 6100; port++ { + http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", port), nil) + } + }() + go func() { + for port := 6070; port < 6100; port++ { + agent.Listen(agent.Options{Addr: fmt.Sprintf("127.0.0.1:%d", port)}) + } + }() + if c.Bool("trace") { + utils.SetLogLevel(logrus.TraceLevel) + } else if c.Bool("debug") { + utils.SetLogLevel(logrus.DebugLevel) + } else if c.Bool("quiet") { + utils.SetLogLevel(logrus.ErrorLevel) + utils.InitLoggers(!c.Bool("nosyslog")) + } + if c.Args().Len() < 1 { + logger.Fatalf("Redis URL and mountpoint are required") + } + addr := c.Args().Get(0) + if !strings.Contains(addr, "://") { + addr = "redis://" + addr + } + if c.Args().Len() < 2 { + logger.Fatalf("MOUNTPOINT is required") + } + mp := c.Args().Get(1) + if !utils.Exists(mp) { + if err := os.MkdirAll(mp, 0777); err != nil { + logger.Fatalf("create %s: %s", mp, err) + } + } + + logger.Infof("Meta address: %s", addr) + var rc = redis.RedisConfig{Retries: 10, Strict: true} + m, err := redis.NewRedisMeta(addr, &rc) + if err != nil { + logger.Fatalf("Meta: %s", err) + } + format, err := m.Load() + if err != nil { + logger.Fatalf("load setting: %s", err) + } + + chunkConf := chunk.Config{ + BlockSize: format.BlockSize * 1024, + Compress: format.Compression, + + GetTimeout: time.Second * time.Duration(c.Int("getTimeout")), + PutTimeout: time.Second * time.Duration(c.Int("putTimeout")), + MaxUpload: c.Int("maxUpload"), + AsyncUpload: c.Bool("writeback"), + Prefetch: c.Int("prefetch"), + BufferSize: c.Int("bufferSize") << 20, + + CacheDir: c.String("cacheDir"), + CacheSize: int64(c.Int("cacheSize")), + FreeSpace: float32(c.Float64("freeRatio")), + CacheMode: os.FileMode(0600), + CacheFullBlock: !c.Bool("partialOnly"), + AutoCreate: true, + } + blob, err := createStorage(format) + if err != nil { + logger.Fatalf("object storage: %s", err) + } + logger.Infof("Data use %s", blob) + logger.Infof("mount volume %s at %s", format.Name, mp) + + if c.Bool("d") { + MakeDaemon() + } + + store := chunk.NewCachedStore(blob, chunkConf) + m.OnMsg(meta.DeleteChunk, meta.MsgCallback(func(args ...interface{}) error { + chunkid := args[0].(uint64) + length := args[1].(uint32) + return store.Remove(chunkid, int(length)) + })) + + conf := &vfs.Config{ + Meta: &meta.Config{ + IORetries: 10, + }, + Format: format, + Version: Version(), + Mountpoint: mp, + Primary: &vfs.StorageConfig{ + Name: format.Storage, + Endpoint: format.Bucket, + AccessKey: format.AccessKey, + SecretKey: format.AccessKey, + }, + Chunk: &chunkConf, + } + vfs.Init(conf, m, store) + + installHandler(mp) + if !c.Bool("no-usage-report") { + go reportUsage(m) + } + err = fuse.Main(conf, c.String("o"), c.Float64("attrcacheto"), c.Float64("entrycacheto"), c.Float64("direntrycacheto")) + if err != nil { + logger.Errorf("%s", err) + os.Exit(1) + } + return nil +} + +func mountFlags() *cli.Command { + var defaultCacheDir = "/var/jfsCache" + if runtime.GOOS == "darwin" { + homeDir, err := os.UserHomeDir() + if err != nil { + logger.Fatalf("%v", err) + return nil + } + defaultCacheDir = path.Join(homeDir, ".juicefs", "cache") + } + return &cli.Command{ + Name: "mount", + Usage: "mount a volume", + ArgsUsage: "REDIS-URL MOUNTPOINT", + Action: mount, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "d", + Usage: "run in background", + }, + &cli.StringFlag{ + Name: "o", + Usage: "other fuse options", + }, + &cli.Float64Flag{ + Name: "attrcacheto", + Value: 1.0, + Usage: "attributes cache timeout in seconds", + }, + &cli.Float64Flag{ + Name: "entrycacheto", + Value: 1.0, + Usage: "file entry cache timeout in seconds", + }, + &cli.Float64Flag{ + Name: "direntrycacheto", + Value: 1.0, + Usage: "dir entry cache timeout in seconds", + }, + + &cli.IntFlag{ + Name: "getTimeout", + Value: 60, + Usage: "the max number of seconds to download an object", + }, + &cli.IntFlag{ + Name: "putTimeout", + Value: 60, + Usage: "the max number of seconds to upload an object", + }, + &cli.IntFlag{ + Name: "ioretries", + Value: 30, + Usage: "number of retries after network failure", + }, + &cli.IntFlag{ + Name: "maxUpload", + Value: 20, + Usage: "number of connections to upload", + }, + &cli.IntFlag{ + Name: "bufferSize", + Value: 300, + Usage: "total read/write buffering in MB", + }, + &cli.IntFlag{ + Name: "prefetch", + Value: 3, + Usage: "prefetch N blocks in parallel", + }, + + &cli.BoolFlag{ + Name: "writeback", + Usage: "Upload objects in background", + }, + &cli.StringFlag{ + Name: "cacheDir", + Value: defaultCacheDir, + Usage: "directory to cache object", + }, + &cli.IntFlag{ + Name: "cacheSize", + Value: 1 << 10, + Usage: "size of cached objects in MiB", + }, + &cli.Float64Flag{ + Name: "freeSpace", + Value: 0.1, + Usage: "min free space (ratio)", + }, + &cli.BoolFlag{ + Name: "partialOnly", + Usage: "cache only random/small read", + }, + + &cli.BoolFlag{ + Name: "no-usage-report", + Usage: "do not send usage report to juicefs.io", + }, + }, + } +} diff --git a/cmd/usage.go b/cmd/usage.go new file mode 100644 index 000000000000..27d95ca22426 --- /dev/null +++ b/cmd/usage.go @@ -0,0 +1,88 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "net/http" + "time" + + "github.com/juicedata/juicefs/pkg/meta" +) + +var reportUrl = "https://juicefs.com/report-usage" + +type usage struct { + VolumeID string `json:"volumeID"` + SessionID int64 `json:"sessionID"` + UsedSpace int64 `json:"usedBytes"` + UsedInodes int64 `json:"usedInodes"` + Version string `json:"version"` + Uptime int64 `json:"uptime"` +} + +func sendUsage(u usage) error { + body, err := json.Marshal(u) + if err != nil { + return err + } + req, err := http.NewRequest("POST", reportUrl, bytes.NewReader(body)) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return fmt.Errorf("got %s", resp.Status) + } + _, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return nil +} + +// reportUsage will send anonymous usage data to juicefs.io to help the team +// understand how the community is using it. You can use `--no-usage-report` +// to disable this. +func reportUsage(m meta.Meta) { + ctx := meta.Background + var u usage + if format, err := m.Load(); err == nil { + u.VolumeID = format.UUID + } + u.SessionID = int64(rand.Uint32()) + u.Version = Version() + var start = time.Now() + for { + var totalSpace, availSpace, iused, iavail uint64 + m.StatFS(ctx, &totalSpace, &availSpace, &iused, &iavail) + u.Uptime = int64(time.Since(start).Seconds()) + u.UsedSpace = int64(totalSpace - availSpace) + u.UsedInodes = int64(iused) + + if err := sendUsage(u); err != nil { + logger.Debugf("send usage: %s", err) + } + time.Sleep(time.Minute * 10) + } +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 000000000000..7db739a5c686 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,28 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package main + +import "fmt" + +var ( + VERSION = "dev" + REVISION = "HEAD" + REVISIONDATE = "now" +) + +func Version() string { + return fmt.Sprintf("%v (%v %v)", VERSION, REVISIONDATE, REVISION) +} diff --git a/docs/fio.md b/docs/fio.md new file mode 100644 index 000000000000..74638beb9ef3 --- /dev/null +++ b/docs/fio.md @@ -0,0 +1,68 @@ +# fio Benchmark + +## Testing Approach + +Performed a sequential read/write benchmark on JuiceFS, [EFS](https://aws.amazon.com/efs) and [S3FS](https://github.com/s3fs-fuse/s3fs-fuse) by [fio](https://github.com/axboe/fio). + +## Testing Tool + +The following tests were performed by fio 3.1. + +Sequential read test (numjobs: 1): + +``` +fio --name=sequential-read --directory=/s3fs --rw=read --refill_buffers --bs=4M --size=4G +fio --name=sequential-read --directory=/efs --rw=read --refill_buffers --bs=4M --size=4G +fio --name=sequential-read --directory=/jfs --rw=read --refill_buffers --bs=4M --size=4G +``` + +Sequential write test (numjobs: 1): + +``` +fio --name=sequential-write --directory=/s3fs --rw=write --refill_buffers --bs=4M --size=4G --end_fsync=1 +fio --name=sequential-write --directory=/efs --rw=write --refill_buffers --bs=4M --size=4G --end_fsync=1 +fio --name=sequential-write --directory=/jfs --rw=write --refill_buffers --bs=4M --size=4G --end_fsync=1 +``` + +Sequential read test (numjobs: 16): + +``` +fio --name=big-file-multi-read --directory=/s3fs --rw=read --refill_buffers --bs=4M --size=4G --numjobs=16 +fio --name=big-file-multi-read --directory=/efs --rw=read --refill_buffers --bs=4M --size=4G --numjobs=16 +fio --name=big-file-multi-read --directory=/jfs --rw=read --refill_buffers --bs=4M --size=4G --numjobs=16 +``` + +Sequential write test (numjobs: 16): + +``` +fio --name=big-file-multi-write --directory=/s3fs --rw=write --refill_buffers --bs=4M --size=4G --numjobs=16 --end_fsync=1 +fio --name=big-file-multi-write --directory=/efs --rw=write --refill_buffers --bs=4M --size=4G --numjobs=16 --end_fsync=1 +fio --name=big-file-multi-write --directory=/jfs --rw=write --refill_buffers --bs=4M --size=4G --numjobs=16 --end_fsync=1 +``` + +## Testing Environment + +In the following test results, all fio tests based on the c5d.18xlarge EC2 instance (72 CPU, 144G RAM), Ubuntu 18.04 LTS (Kernel 5.4.0) system, JuiceFS use the local Redis instance (version 4.0.9) to store metadata. + +JuiceFS mount command: + +``` +./juicefs format --storage=s3 --bucket=https://.s3..amazonaws.com localhost benchmark +./juicefs mount --maxUpload=150 --ioretries=20 localhost /jfs +``` + +EFS mount command (the same as the configuration page): + +``` +mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport, .efs..amazonaws.com:/ /efs +``` + +S3FS (version 1.82) mount command: + +``` +s3fs :/s3fs /s3fs -o host=https://s3..amazonaws.com,endpoint=,passwd_file=${HOME}/.passwd-s3fs +``` + +## Testing Result + +![Sequential Read Write Benchmark](../docs/images/sequential-read-write-benchmark.svg) diff --git a/docs/images/juicefs-arch.png b/docs/images/juicefs-arch.png new file mode 100644 index 0000000000000000000000000000000000000000..8191386dfadf1477a1e96d4ce8d19f8d094c6a8c GIT binary patch literal 172120 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 literal 0 HcmV?d00001 diff --git a/docs/images/juicefs-storage-format.png b/docs/images/juicefs-storage-format.png new file mode 100644 index 0000000000000000000000000000000000000000..adfe4331b78ab079db08ce64e90c253a92b376ac GIT binary patch literal 113469 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%0A0#7RW8 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 QtpfjykC`9MHbnmZUp->=Qvd(} literal 0 HcmV?d00001 diff --git a/docs/images/metadata-benchmark.svg b/docs/images/metadata-benchmark.svg new file mode 100644 index 000000000000..83273aa8fa14 --- /dev/null +++ b/docs/images/metadata-benchmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/sequential-read-write-benchmark.svg b/docs/images/sequential-read-write-benchmark.svg new file mode 100644 index 000000000000..4826a704d1be --- /dev/null +++ b/docs/images/sequential-read-write-benchmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/mdtest.md b/docs/mdtest.md new file mode 100644 index 000000000000..e7d218298958 --- /dev/null +++ b/docs/mdtest.md @@ -0,0 +1,117 @@ +# mdtest Benchmark + +## Testing Approach + +Performed a metadata test on JuiceFS, [EFS](https://aws.amazon.com/efs) and [S3FS](https://github.com/s3fs-fuse/s3fs-fuse) by [mdtest](https://github.com/hpc/ior). + +## Testing Tool + +The following tests were performed by mdtest 3.4. +Arguments of mdtest are adjusted to ensure the command can be finished in 5 minutes. + +``` +./mdtest -d /s3fs/mdtest -b 6 -I 8 -z 2 +./mdtest -d /efs/mdtest -b 6 -I 8 -z 4 +./mdtest -d /jfs/mdtest -b 6 -I 8 -z 4 +``` + +## Testing Environment + +In the following test results, all mdtest tests based on the c5.large EC2 instance (2 CPU, 4G RAM), Ubuntu 18.04 LTS (Kernel 5.4.0) system, JuiceFS use redis (version 4.0.9) running on a c5.large EC2 instance in the same available zone to store metadata. + +JuiceFS mount command: + +``` +./juicefs format --storage=s3 --bucket=https://.s3..amazonaws.com localhost benchmark +nohup ./juicefs mount localhost /jfs & +``` + +EFS mount command (the same as the configuration page): + +``` +mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport, .efs..amazonaws.com:/ /efs +``` + +S3FS (version 1.82) mount command: + +``` +s3fs :/s3fs /s3fs -o host=https://s3..amazonaws.com,endpoint=,passwd_file=${HOME}/.passwd-s3fs +``` + +## Testing Result + +![Metadata Benchmark](../docs/images/metadata-benchmark.svg) + +### S3FS +``` +mdtest-3.4.0+dev was launched with 1 total task(s) on 1 node(s) +Command line used: ./mdtest '-d' '/s3fs/mdtest' '-b' '6' '-I' '8' '-z' '2' +WARNING: Read bytes is 0, thus, a read test will actually just open/close. +Path : /s3fs/mdtest +FS : 256.0 TiB Used FS: 0.0% Inodes: 0.0 Mi Used Inodes: -nan% +Nodemap: 1 +1 tasks, 344 files/directories + +SUMMARY rate: (of 1 iterations) + Operation Max Min Mean Std Dev + --------- --- --- ---- ------- + Directory creation : 5.977 5.977 5.977 0.000 + Directory stat : 435.898 435.898 435.898 0.000 + Directory removal : 8.969 8.969 8.969 0.000 + File creation : 5.696 5.696 5.696 0.000 + File stat : 68.692 68.692 68.692 0.000 + File read : 33.931 33.931 33.931 0.000 + File removal : 23.658 23.658 23.658 0.000 + Tree creation : 5.951 5.951 5.951 0.000 + Tree removal : 9.889 9.889 9.889 0.000 +``` + +### EFS + +``` +mdtest-3.4.0+dev was launched with 1 total task(s) on 1 node(s) +Command line used: ./mdtest '-d' '/efs/mdtest' '-b' '6' '-I' '8' '-z' '4' +WARNING: Read bytes is 0, thus, a read test will actually just open/close. +Path : /efs/mdtest +FS : 8388608.0 TiB Used FS: 0.0% Inodes: 0.0 Mi Used Inodes: -nan% +Nodemap: 1 +1 tasks, 12440 files/directories + +SUMMARY rate: (of 1 iterations) + Operation Max Min Mean Std Dev + --------- --- --- ---- ------- + Directory creation : 192.301 192.301 192.301 0.000 + Directory stat : 1311.166 1311.166 1311.166 0.000 + Directory removal : 213.132 213.132 213.132 0.000 + File creation : 179.293 179.293 179.293 0.000 + File stat : 915.230 915.230 915.230 0.000 + File read : 371.012 371.012 371.012 0.000 + File removal : 217.498 217.498 217.498 0.000 + Tree creation : 187.906 187.906 187.906 0.000 + Tree removal : 218.357 218.357 218.357 0.000 +``` + +### JuiceFS + +``` +mdtest-3.4.0+dev was launched with 1 total task(s) on 1 node(s) +Command line used: ./mdtest '-d' '/jfs/mdtest' '-b' '6' '-I' '8' '-z' '4' +WARNING: Read bytes is 0, thus, a read test will actually just open/close. +Path : /jfs/mdtest +FS : 1024.0 TiB Used FS: 0.0% Inodes: 10.0 Mi Used Inodes: 0.0% +Nodemap: 1 +1 tasks, 12440 files/directories + +SUMMARY rate: (of 1 iterations) + Operation Max Min Mean Std Dev + --------- --- --- ---- ------- + Directory creation : 1416.582 1416.582 1416.582 0.000 + Directory stat : 3810.083 3810.083 3810.083 0.000 + Directory removal : 1115.108 1115.108 1115.108 0.000 + File creation : 1410.288 1410.288 1410.288 0.000 + File stat : 5023.227 5023.227 5023.227 0.000 + File read : 3487.947 3487.947 3487.947 0.000 + File removal : 1163.371 1163.371 1163.371 0.000 + Tree creation : 1503.004 1503.004 1503.004 0.000 + Tree removal : 1119.806 1119.806 1119.806 0.000 +``` diff --git a/fstests/Makefile b/fstests/Makefile new file mode 100644 index 000000000000..42de0c667c36 --- /dev/null +++ b/fstests/Makefile @@ -0,0 +1,38 @@ +DURATION ?= 10 + +all: fsracer fsx xattrs + +xattrs: + touch /jfs/test_xattrs + setfattr -n user.k -v value /jfs/test_xattrs + getfattr -n user.k /jfs/test_xattrs | grep -q user.k= + +fsracer: healthcheck secfs.test/tools/bin/fsracer + secfs.test/tools/bin/fsracer $(DURATION) /jfs >fsracer.log + make healthcheck + +fsx: healthcheck secfs.test/tools/bin/fsx + secfs.test/tools/bin/fsx -d $(DURATION) -p 10000 -F 10000000 /tmp/fsx.out + make healthcheck + +setup: + redis-server & + mkdir -p /jfs + ../juicefs format -storage mem localhost unittest + ../juicefs mount localhost /jfs & + +healthcheck: + pgrep juicefs + +secfs.test/tools/bin/fsx: secfs.test + +secfs.test/tools/bin/fsracer: secfs.test + +secfs.test: + git clone https://github.com/billziss-gh/secfs.test.git + make -C secfs.test >secfs.test-build.log 2>&1 + +flock: + git clone https://github.com/gofrs/flock.git + mkdir /jfs/tmp + cd flock && TMPDIR=/jfs/tmp go test . diff --git a/go.mod b/go.mod new file mode 100644 index 000000000000..3de3e2bc158e --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/juicedata/juicefs + +go 1.13 + +require ( + github.com/DataDog/zstd v1.4.5 + github.com/VividCortex/godaemon v0.0.0-20201215173923-eda977734e72 + github.com/go-redis/redis/v8 v8.4.0 + github.com/google/gops v0.3.13 + github.com/google/uuid v1.1.1 + github.com/hanwen/go-fuse/v2 v2.0.4-0.20210104155004-09a3c381714c + github.com/hungys/go-lz4 v0.0.0-20170805124057-19ff7f07f099 + github.com/juicedata/juicesync v0.6.3-0.20210105123925-2af95f8a8472 + github.com/sirupsen/logrus v1.7.0 + github.com/urfave/cli/v2 v2.3.0 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000000..81c5aeb6f943 --- /dev/null +++ b/go.sum @@ -0,0 +1,265 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.39.0 h1:UgQP9na6OTfp4dsAiz/eFpFA1C6tPdH5wiRdi19tuMw= +cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= +github.com/Azure/azure-sdk-for-go v11.1.1-beta+incompatible h1:UanIfAyKxwQgLNfs8LIVfWsSB6JaA0Bj5grnEldOFok= +github.com/Azure/azure-sdk-for-go v11.1.1-beta+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest v8.4.0+incompatible h1:L0lfRrBLK26wwXV0pkmu7D2w1n2VBU6QaH0DXGLvnzA= +github.com/Azure/go-autorest v8.4.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/IBM/ibm-cos-sdk-go v1.6.0 h1:09jPKbUZw5XX6YT0cpQHpjS0Lq+YZrv3+ApQy/skKKc= +github.com/IBM/ibm-cos-sdk-go v1.6.0/go.mod h1:Pa7XzoyngeWPnqGol8ZF+gwUeLEAyDHkkgIJ79dXARY= +github.com/NetEase-Object-Storage/nos-golang-sdk v0.0.0-20171031020902-cc8892cb2b05 h1:NEPjpPSOSDDmnix+VANw/CfUs1fAorLIaz/IFz2eQ2o= +github.com/NetEase-Object-Storage/nos-golang-sdk v0.0.0-20171031020902-cc8892cb2b05/go.mod h1:0N5CbwYI/8V1T6YOEwkgMvLmiGDNn661vLutBZQrC2c= +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VividCortex/godaemon v0.0.0-20201215173923-eda977734e72 h1:kPET0ZTtmPg+0obS7L3KWPL9IqqgvMNhZ6Rkxj3iIpA= +github.com/VividCortex/godaemon v0.0.0-20201215173923-eda977734e72/go.mod h1:Y8CJ3IwPIAkMhv/rRUWIlczaeqd9ty9yrl+nc2AbaL4= +github.com/aliyun/aliyun-oss-go-sdk v2.1.0+incompatible h1:90Z2Cp7EqcbaYfVwVjmQoK8kgoFPz+doQlujcwe1BRg= +github.com/aliyun/aliyun-oss-go-sdk v2.1.0+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/aws/aws-sdk-go v1.12.10 h1:ihg0UOujHVcFciyc6zs/q5VLhoG1K+oDLqgpCxkAh04= +github.com/aws/aws-sdk-go v1.12.10/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k= +github.com/baidubce/bce-sdk-go v0.0.0-20180401121131-aa0c7bd66b01 h1:pmQ6WjFOHtNL0IHsLB0r3fOyRQ6KK/CfZ9dDI7ugZnU= +github.com/baidubce/bce-sdk-go v0.0.0-20180401121131-aa0c7bd66b01/go.mod h1:T3yEA2H7hXAlvniSEJRsPlDYlh8OEZZzH0zlIP/1JIY= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/colinmarc/hdfs/v2 v2.2.0 h1:4AaIlTq+/sWmeqYhI0dX8bD4YrMQM990tRjm636FkGM= +github.com/colinmarc/hdfs/v2 v2.2.0/go.mod h1:Wss6n3mtaZyRwWaqtSH+6ge01qT0rw9dJJmvoUnIQ/E= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.0.0+incompatible h1:nfVqwkkhaRUethVJaQf5TUFdFr3YUF4lJBTf/F2XwVI= +github.com/dgrijalva/jwt-go v3.0.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-ini/ini v1.28.2 h1:drmmYv7psRpoGZkPtPKKTB+ZFSnvmwCMfNj5o1nLh2Y= +github.com/go-ini/ini v1.28.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-redis/redis/v8 v8.4.0 h1:J5NCReIgh3QgUJu398hUncxDExN4gMOHI11NVbVicGQ= +github.com/go-redis/redis/v8 v8.4.0/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gops v0.3.13 h1:8lgvDd3tXe4UxVbmPPTGE0ToIpbh3hgXkt4EVZ8Y/hU= +github.com/google/gops v0.3.13/go.mod h1:38bMPVKFh+1X106CPpbLAWtZIR1+xwgzT9gew0kn6w4= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc= +github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= +github.com/hanwen/go-fuse/v2 v2.0.4-0.20210104155004-09a3c381714c h1:iyvcTTLELLcFVDxx5b3n0O7XosPY9SQsq7tP4LyLib0= +github.com/hanwen/go-fuse/v2 v2.0.4-0.20210104155004-09a3c381714c/go.mod h1:0EQM6aH2ctVpvZ6a+onrQ/vaykxh2GH7hy3e13vzTUY= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huaweicloud/huaweicloud-sdk-go-obs v0.0.0-20190127152727-3a9e1f8023d5 h1:9cpB9emicKlvo6mRm/fYEcTkiM8SPgJ+gTc/GXQH6V8= +github.com/huaweicloud/huaweicloud-sdk-go-obs v0.0.0-20190127152727-3a9e1f8023d5/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= +github.com/hungys/go-lz4 v0.0.0-20170805124057-19ff7f07f099 h1:heHZCso/ytvpYr+hp2cDxlZfA/jTw46aHSvT9kZnJ7o= +github.com/hungys/go-lz4 v0.0.0-20170805124057-19ff7f07f099/go.mod h1:h44tqw4M3GN0Woo9KBStxJxm8huNi+9+tOHoeqSvhaY= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.1/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZgZdoFrZaZNM= +github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juicedata/juicesync v0.6.3-0.20210105123925-2af95f8a8472 h1:4IWRyIhlkU2+0UhJcVdLfwjxnBMSj91TIgDN/2E0ikM= +github.com/juicedata/juicesync v0.6.3-0.20210105123925-2af95f8a8472/go.mod h1:PTOFdso2hrHXU4RctCWgbSysCclKAjcAU311/z55lUE= +github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= +github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/ks3sdklib/aws-sdk-go v0.0.0-20180820074416-dafab05ad142 h1:GJhAJadgNsHvO7dMqa+GkV3CxooJujxZXVj9VwhwSmI= +github.com/ks3sdklib/aws-sdk-go v0.0.0-20180820074416-dafab05ad142/go.mod h1:WKPC0Foi1kjnyeC6Ei45XBBT+CIzHuhk/uwpCRmAf+o= +github.com/kurin/blazer v0.2.1 h1:lUhpcdTHl3foU5IcjgzM5Hbv9hQX7ce7PugSGIi+ztU= +github.com/kurin/blazer v0.2.1/go.mod h1:4FCXMUWo9DllR2Do4TtBd377ezyAJ51vB5uTBjt0pGU= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/pborman/getopt v0.0.0-20180729010549-6fdd0a2c7117/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14 h1:XeOYlK9W1uCmhjJSsY78Mcuh7MVkNjTzmHx1yBzizSU= +github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14/go.mod h1:jVblp62SafmidSkvWrXyxAme3gaTfEtWwRPGz5cpvHg= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.0 h1:DGA1KlA9esU6WcicH+P8PxFZOl15O6GYtab1cIJdOlE= +github.com/pkg/sftp v1.10.0/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/qiniu/api.v7/v7 v7.8.0 h1:Ye9sHXwCpeDgKJ4BNSoDvXe4yEuU8a/HTT1jKRgkqe8= +github.com/qiniu/api.v7/v7 v7.8.0/go.mod h1:J7pD9UsnxO7XxyRLUHpsWEQd/HgWJNwnn/Za9qEPdEA= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/satori/uuid v1.1.0 h1:ZS7eEEVHlX8VYf4sjZMpx4RO5emTVEAZn99aO+uBFXI= +github.com/satori/uuid v1.1.0/go.mod h1:B8HLsPLik/YNn6KKWVMDJ8nzCL8RP5WyfsnmvnAEwIU= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/shirou/gopsutil v2.20.4+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.3/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tencentyun/cos-go-sdk-v5 v0.7.8 h1:BeqN3uNCyYgoujWqZDbpQMhNmPf5xIypjzbT2AMMZUs= +github.com/tencentyun/cos-go-sdk-v5 v0.7.8/go.mod h1:wQBO5HdAkLjj2q6XQiIfDSP8DXDNrppDRw2Kp/1BODA= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8 h1:EVObHAr8DqpoJCVv6KYTle8FEImKhtkfcZetNqxDoJQ= +github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= +github.com/xlab/treeprint v1.0.0/go.mod h1:IoImgRak9i3zJyuxOKUP1v4UZd1tMoKkq/Cimt1uhCg= +github.com/yunify/qingstor-sdk-go v2.2.15+incompatible h1:/Z0q3/eSMoPYAuRmhjWtuGSmVVciFC6hfm3yfCKuvz0= +github.com/yunify/qingstor-sdk-go v2.2.15+incompatible/go.mod h1:w6wqLDQ5bBTzxGJ55581UrSwLrsTAsdo9N6yX/8d9RY= +github.com/yunify/qingstor-sdk-go/v3 v3.1.1/go.mod h1:KciFNuMu6F4WLk9nGwwK69sCGKLCdd9f97ac/wfumS4= +go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opentelemetry.io/otel v0.14.0 h1:YFBEfjCk9MTjaytCNSUkp9Q8lF7QJezA06T71FbQxLQ= +go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190517181255-950ef44c6e07 h1:XC1K3wNjuz44KaI+cj85C9TW85w/46RH7J+DTXNH5Wk= +golang.org/x/oauth2 v0.0.0-20190517181255-950ef44c6e07/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM= +google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8 h1:x913Lq/RebkvUmRSdQ8MNb0GZKn+SR1ESfoetcQSeak= +google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= diff --git a/pkg/chunk/cached_store.go b/pkg/chunk/cached_store.go new file mode 100644 index 000000000000..4d037261c290 --- /dev/null +++ b/pkg/chunk/cached_store.go @@ -0,0 +1,780 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/juicedata/juicefs/pkg/utils" + + "github.com/juicedata/juicesync/object" +) + +const chunkSize = 1 << 26 // 64M +const pageSize = 1 << 16 // 64K +const SlowRequest = time.Second * time.Duration(10) + +var ( + logger = utils.GetLogger("juicefs") +) + +// chunk for read only +type rChunk struct { + id uint64 + length int + store *cachedStore +} + +func chunkForRead(id uint64, length int, store *cachedStore) *rChunk { + return &rChunk{id, length, store} +} + +func (c *rChunk) blockSize(indx int) int { + bsize := c.length - indx*c.store.conf.BlockSize + if bsize > c.store.conf.BlockSize { + bsize = c.store.conf.BlockSize + } + return bsize +} + +func (c *rChunk) key(indx int) string { + if c.store.conf.Partitions > 1 { + return fmt.Sprintf("chunks/%02X/%v/%v_%v_%v", c.id%256, c.id/1000/1000, c.id, indx, c.blockSize(indx)) + } + return fmt.Sprintf("chunks/%v/%v/%v_%v_%v", c.id/1000/1000, c.id/1000, c.id, indx, c.blockSize(indx)) +} + +func (c *rChunk) index(off int) int { + return off / c.store.conf.BlockSize +} + +func (c *rChunk) loadPage(ctx context.Context, indx int) (b *Page, err error) { + key := c.key(indx) + var block []byte + for i := 0; i < 3 && block == nil; i++ { + block, err = c.store.Get(key) + time.Sleep(time.Second * time.Duration(i*i)) + } + if err != nil { + return nil, err + } + return NewPage(block), nil +} + +func (c *rChunk) ReadAt(ctx context.Context, page *Page, off int) (n int, err error) { + p := page.Data + if len(p) == 0 { + return 0, nil + } + if int(off) >= c.length { + return 0, io.EOF + } + + indx := c.index(off) + boff := int(off) % c.store.conf.BlockSize + blockSize := c.blockSize(indx) + if boff+len(p) > blockSize { + // read beyond currend page + var got int + for got < len(p) { + // aligned to current page + l := utils.Min(len(p)-got, c.blockSize(c.index(off))-int(off)%c.store.conf.BlockSize) + pp := page.Slice(got, l) + n, err = c.ReadAt(ctx, pp, off) + pp.Release() + if err != nil { + return got + n, err + } + if n == 0 { + return got, io.EOF + } + got += n + off += n + } + return got, nil + } + + key := c.key(indx) + if c.store.conf.CacheSize > 0 { + r, err := c.store.bcache.load(key) + if err == nil { + n, err = r.ReadAt(p, int64(boff)) + r.Close() + if err == nil { + return n, nil + } + if f, ok := r.(*os.File); ok { + logger.Warnf("remove partial cached block %s: %d %s", f.Name(), n, err) + os.Remove(f.Name()) + } + } + } + + if !c.store.shouldCache(len(p)) { + if c.store.seekable && boff > 0 && len(p) <= blockSize/4 { + // partial read + st := time.Now() + in, err := c.store.storage.Get(key, int64(boff), int64(len(p))) + used := time.Since(st) + logger.Debugf("GET %s RANGE(%d,%d) (%s, %.3fs)", key, boff, len(p), err, used.Seconds()) + if used > SlowRequest { + logger.Infof("slow request: GET %s (%s, %.3fs)", key, err, used.Seconds()) + } + c.store.fetcher.fetch(key) + if err == nil { + defer in.Close() + return io.ReadFull(in, p) + } + } + block, err := c.store.group.Execute(key, func() (*Page, error) { + tmp := page + if boff > 0 || len(p) < blockSize { + tmp = NewOffPage(blockSize) + } else { + tmp.Acquire() + } + tmp.Acquire() + err := withTimeout(func() error { + defer tmp.Release() + return c.store.load(key, tmp, c.store.shouldCache(blockSize)) + }, c.store.conf.GetTimeout) + return tmp, err + }) + defer block.Release() + if err != nil { + return 0, err + } + if block != page { + copy(p, block.Data[boff:]) + } + return len(p), nil + } + + block, err := c.loadPage(ctx, indx) + if err != nil { + return 0, err + } + defer block.Release() + n = copy(p, block.Data[boff:]) + return n, nil +} + +func (c *rChunk) delete(indx int) error { + key := c.key(indx) + st := time.Now() + err := c.store.storage.Delete(key) + used := time.Since(st) + logger.Debugf("DELETE %v (%v, %.3fs)", key, err, used.Seconds()) + if used > SlowRequest { + logger.Infof("slow request: DELETE %v (%s, %.3fs)", key, err, used.Seconds()) + } + return err +} + +func (c *rChunk) Remove() error { + if c.length == 0 { + // no block + return nil + } + + lastIndx := (c.length - 1) / c.store.conf.BlockSize + deleted := false + for i := 0; i <= lastIndx; i++ { + // there could be multiple clients try to remove the same chunk in the same time, + // any of them should succeed if any blocks is removed + key := c.key(i) + c.store.pendingMutex.Lock() + delete(c.store.pendingKeys, key) + c.store.pendingMutex.Unlock() + c.store.bcache.remove(key) + if c.delete(i) == nil { + deleted = true + } + } + + if !deleted { + return errors.New("chunk not found") + } + return nil +} + +var pagePool = make(chan *Page, 128) + +func allocPage(sz int) *Page { + if sz != pageSize { + return NewOffPage(sz) + } + select { + case p := <-pagePool: + return p + default: + return NewOffPage(pageSize) + } +} + +func freePage(p *Page) { + if cap(p.Data) != pageSize { + p.Release() + return + } + select { + case pagePool <- p: + default: + p.Release() + } +} + +// chunk for write only +type wChunk struct { + rChunk + pages [][]*Page + uploaded int + errors chan error + uploadError error + pendings int +} + +func chunkForWrite(id uint64, store *cachedStore) *wChunk { + return &wChunk{ + rChunk: rChunk{id, 0, store}, + pages: make([][]*Page, chunkSize/store.conf.BlockSize), + errors: make(chan error, chunkSize/store.conf.BlockSize), + } +} + +func (c *wChunk) SetID(id uint64) { + c.id = id +} + +func (c *wChunk) WriteAt(p []byte, off int64) (n int, err error) { + if int(off)+len(p) > chunkSize { + return 0, fmt.Errorf("write out of chunk boudary: %d > %d", int(off)+len(p), chunkSize) + } + if off < int64(c.uploaded) { + return 0, fmt.Errorf("Cannot overwrite uploaded block: %d < %d", off, c.uploaded) + } + + // Fill previous blocks with zeros + if c.length < int(off) { + zeros := make([]byte, int(off)-c.length) + c.WriteAt(zeros, int64(c.length)) + } + + for n < len(p) { + indx := c.index(int(off) + n) + boff := (int(off) + n) % c.store.conf.BlockSize + var bs = pageSize + if indx > 0 || bs > c.store.conf.BlockSize { + bs = c.store.conf.BlockSize + } + bi := boff / bs + bo := boff % bs + var page *Page + if bi < len(c.pages[indx]) { + page = c.pages[indx][bi] + } else { + page = allocPage(bs) + page.Data = page.Data[:0] + c.pages[indx] = append(c.pages[indx], page) + } + left := len(p) - n + if bo+left > bs { + page.Data = page.Data[:bs] + } else if len(page.Data) < bo+left { + page.Data = page.Data[:bo+left] + } + n += copy(page.Data[bo:], p[n:]) + } + if int(off)+n > c.length { + c.length = int(off) + n + } + return n, nil +} + +func withTimeout(f func() error, timeout time.Duration) error { + var done = make(chan int, 1) + var t = time.NewTimer(timeout) + var err error + go func() { + err = f() + done <- 1 + }() + select { + case <-done: + t.Stop() + case <-t.C: + err = fmt.Errorf("timeout after %s", timeout) + } + return err +} + +func (c *wChunk) put(key string, p *Page) error { + p.Acquire() + return withTimeout(func() error { + defer p.Release() + st := time.Now() + err := c.store.storage.Put(key, bytes.NewReader(p.Data)) + used := time.Since(st) + logger.Debugf("PUT %s (%s, %.3fs)", key, err, used.Seconds()) + if used > SlowRequest { + logger.Infof("slow request: PUT %v (%s, %.3fs)", key, err, used.Seconds()) + } + return err + }, c.store.conf.PutTimeout) +} + +func (c *wChunk) syncUpload(key string, block *Page) { + blen := len(block.Data) + bufSize := c.store.compressor.CompressBound(blen) + var buf *Page + if bufSize > blen { + buf = NewOffPage(bufSize) + } else { + buf = block + buf.Acquire() + } + n, err := c.store.compressor.Compress(buf.Data, block.Data) + if err != nil { + logger.Fatalf("compress chunk %v: %s", c.id, err) + return + } + buf.Data = buf.Data[:n] + if blen < c.store.conf.BlockSize { + // block will be freed after written into disk + c.store.bcache.cache(key, block) + } + block.Release() + + c.store.currentUpload <- true + defer func() { + buf.Release() + <-c.store.currentUpload + }() + + try := 0 + for try <= 10 && c.uploadError == nil { + err = c.put(key, buf) + if err == nil { + c.errors <- nil + return + } + try++ + logger.Warnf("upload %s: %s (try %d)", key, err, try) + time.Sleep(time.Second * time.Duration(try*try)) + } + c.errors <- fmt.Errorf("upload block %s: %s (after %d tries)", key, err, try) +} + +func (c *wChunk) asyncUpload(key string, block *Page, stagingPath string) { + blockSize := len(block.Data) + defer c.store.bcache.uploaded(key, blockSize) + defer func() { + <-c.store.currentUpload + }() + select { + case c.store.currentUpload <- true: + default: + // release the memory and wait + block.Release() + c.store.pendingMutex.Lock() + c.store.pendingKeys[key] = true + c.store.pendingMutex.Unlock() + defer func() { + c.store.pendingMutex.Lock() + delete(c.store.pendingKeys, key) + c.store.pendingMutex.Unlock() + }() + + logger.Debugf("wait to upload %s", key) + c.store.currentUpload <- true + + // load from disk + f, err := os.Open(stagingPath) + if err != nil { + c.store.pendingMutex.Lock() + ok := c.store.pendingKeys[key] + c.store.pendingMutex.Unlock() + if ok { + logger.Errorf("read stagging file %s: %s", stagingPath, err) + } else { + logger.Debugf("%s is not needed, drop it", key) + } + return + } + + block = NewOffPage(blockSize) + _, err = io.ReadFull(f, block.Data) + f.Close() + if err != nil { + logger.Errorf("read stagging file %s: %s", stagingPath, err) + block.Release() + return + } + } + bufSize := c.store.compressor.CompressBound(blockSize) + var buf *Page + if bufSize > blockSize { + buf = NewOffPage(bufSize) + } else { + buf = block + buf.Acquire() + } + n, err := c.store.compressor.Compress(buf.Data, block.Data) + if err != nil { + logger.Fatalf("compress chunk %v: %s", c.id, err) + return + } + buf.Data = buf.Data[:n] + block.Release() + + try := 0 + for c.uploadError == nil { + err = c.put(key, buf) + if err == nil { + break + } + logger.Warnf("upload %s: %s (tried %d)", key, err, try) + try++ + time.Sleep(time.Second * time.Duration(try)) + } + buf.Release() + os.Remove(stagingPath) +} + +func (c *wChunk) upload(indx int) { + blen := c.blockSize(indx) + key := c.key(indx) + pages := c.pages[indx] + c.pages[indx] = nil + c.pendings++ + + go func() { + var block *Page + if len(pages) == 1 { + block = pages[0] + } else { + block = NewOffPage(blen) + var off int + for _, b := range pages { + off += copy(block.Data[off:], b.Data) + freePage(b) + } + if off != blen { + logger.Fatalf("block length does not match: %v != %v", off, blen) + } + } + if c.store.conf.AsyncUpload { + stagingPath, err := c.store.bcache.stage(key, block.Data, c.store.shouldCache(blen)) + if err != nil { + logger.Warnf("write %s to disk: %s, upload it directly", stagingPath, err) + c.syncUpload(key, block) + } else { + c.errors <- nil + go c.asyncUpload(key, block, stagingPath) + } + } else { + c.syncUpload(key, block) + } + }() +} + +func (c *wChunk) ID() uint64 { + return c.id +} + +func (c *wChunk) Len() int { + return c.length +} + +func (c *wChunk) FlushTo(offset int) error { + if offset < c.uploaded { + logger.Fatalf("Invalid offset: %d < %d", offset, c.uploaded) + } + for i, block := range c.pages { + start := i * c.store.conf.BlockSize + end := start + c.store.conf.BlockSize + if start >= c.uploaded && end <= offset { + if block != nil { + c.upload(i) + } + c.uploaded = end + } + } + + return nil +} + +func (c *wChunk) Finish(length int) error { + if c.length != length { + return fmt.Errorf("Length mismatch: %v != %v", c.length, length) + } + + n := (length-1)/c.store.conf.BlockSize + 1 + if err := c.FlushTo(n * c.store.conf.BlockSize); err != nil { + return err + } + for i := 0; i < c.pendings; i++ { + if err := <-c.errors; err != nil { + c.uploadError = err + return err + } + } + return nil +} + +func (c *wChunk) Abort() { + for i := range c.pages { + for _, b := range c.pages[i] { + freePage(b) + } + c.pages[i] = nil + } +} + +// Config contains options for cachedStore +type Config struct { + CacheDir string + CacheMode os.FileMode + CacheSize int64 + FreeSpace float32 + AutoCreate bool + Compress string + MaxUpload int + AsyncUpload bool + Partitions int + BlockSize int + UploadLimit int + GetTimeout time.Duration + PutTimeout time.Duration + CacheFullBlock bool + BufferSize int + Readahead int + Prefetch int +} + +type cachedStore struct { + storage object.ObjectStorage + bcache CacheManager + fetcher *prefetcher + conf Config + group *Controller + currentUpload chan bool + pendingKeys map[string]bool + pendingMutex sync.Mutex + compressor utils.Compressor + seekable bool +} + +func (store *cachedStore) load(key string, page *Page, cache bool) (err error) { + defer func() { + e := recover() + if e != nil { + err = fmt.Errorf("recovered from %s", e) + } + }() + + err = errors.New("Not downloaded") + var in io.ReadCloser + tried := 0 + start := time.Now() + // it will be retried outside + for err != nil && tried < 2 { + time.Sleep(time.Second * time.Duration(tried*tried)) + st := time.Now() + in, err = store.storage.Get(key, 0, -1) + used := time.Since(st) + logger.Debugf("GET %s (%s, %.3fs)", key, err, used.Seconds()) + if used > SlowRequest { + logger.Infof("slow request: GET %s (%s, %.3fs)", key, err, used.Seconds()) + } + tried++ + } + if err != nil { + return fmt.Errorf("get %s: %s", key, err) + } + needed := store.compressor.CompressBound(len(page.Data)) + var n int + if needed > len(page.Data) { + c := NewOffPage(needed) + defer c.Release() + var cn int + cn, err = io.ReadFull(in, c.Data) + in.Close() + if err != nil && (cn == 0 || err != io.ErrUnexpectedEOF) { + return err + } + n, err = store.compressor.Decompress(page.Data, c.Data[:cn]) + } else { + n, err = io.ReadFull(in, page.Data) + } + if err != nil || n < len(page.Data) { + return fmt.Errorf("read %s fully: %s (%d < %d) after %s (tried %d)", key, err, n, len(page.Data), + time.Since(start), tried) + } + if cache { + store.bcache.cache(key, page) + } + return nil +} + +func (store *cachedStore) Get(key string) (result []byte, err error) { + err = withTimeout(func() error { + var boff, limit int + if strings.Contains(key, ",") { + parts := strings.SplitN(key, ",", 3) + key = parts[0] + boff, _ = strconv.Atoi(parts[1]) + limit, _ = strconv.Atoi(parts[2]) + } + size := parseObjOrigSize(key) + if size == 0 || size > store.conf.BlockSize { + logger.Fatalf("Invalid key: %s", key) + } + if limit == 0 { + limit = size + } + r, err := store.bcache.load(key) + if err == nil { + // TODO: use page + block := make([]byte, limit) + n, err := r.ReadAt(block, int64(boff)) + r.Close() + if err == nil { + result = block + return nil + } + if f, ok := r.(*os.File); ok { + logger.Errorf("short chunk %s: %d < %d", key, n, size) + os.Remove(f.Name()) + } + } + block := make([]byte, size) + err = store.load(key, NewPage(block), true) + if err == nil { + result = block[boff : boff+limit] + } + return err + }, store.conf.GetTimeout) + return result, err +} + +// NewCachedStore create a cached store. +func NewCachedStore(storage object.ObjectStorage, config Config) ChunkStore { + compressor := utils.NewCompressor(config.Compress) + if compressor == nil { + logger.Fatalf("unknown compress algorithm: %s", config.Compress) + } + if config.GetTimeout == 0 { + config.GetTimeout = time.Second * 60 + } + if config.PutTimeout == 0 { + config.PutTimeout = time.Second * 60 + } + store := &cachedStore{ + storage: storage, + conf: config, + currentUpload: make(chan bool, config.MaxUpload), + compressor: compressor, + seekable: compressor.CompressBound(0) == 0, + bcache: newCacheManager(&config), + pendingKeys: make(map[string]bool), + group: &Controller{}, + } + if config.CacheSize == 0 { + config.Prefetch = 0 // disable prefetch if cache is disabled + } + store.fetcher = newPrefetcher(config.Prefetch, func(key string) { + store.Get(key) + }) + go store.uploadStaging() + return store +} + +func (c *cachedStore) shouldCache(size int) bool { + return size < c.conf.BlockSize || c.conf.CacheFullBlock +} + +func parseObjOrigSize(key string) int { + p := strings.LastIndexByte(key, '_') + l, _ := strconv.Atoi(key[p+1:]) + return l +} + +func (s *cachedStore) uploadStaging() { + staging := s.bcache.scanStaging() + for key, path := range staging { + s.currentUpload <- true + go func(key, stagingPath string) { + defer func() { + <-s.currentUpload + }() + block, err := ioutil.ReadFile(stagingPath) + if err != nil { + logger.Errorf("open %s: %s", stagingPath, err) + return + } + buf := make([]byte, s.compressor.CompressBound(len(block))) + n, err := s.compressor.Compress(buf, block) + if err != nil { + logger.Errorf("compress chunk %s: %s", stagingPath, err) + return + } + compressed := buf[:n] + + if strings.Count(key, "_") == 1 { + // add size at the end + key = fmt.Sprintf("%s_%d", key, len(block)) + } + try := 0 + for { + err := s.storage.Put(key, bytes.NewReader(compressed)) + if err == nil { + break + } + logger.Infof("upload %s: %s (try %d)", key, err, try) + try++ + time.Sleep(time.Second * time.Duration(try*try)) + } + s.bcache.uploaded(key, len(block)) + os.Remove(stagingPath) + }(key, path) + } +} + +func (s *cachedStore) NewReader(chunkid uint64, length int) Reader { + return chunkForRead(chunkid, length, s) +} + +func (s *cachedStore) NewWriter(chunkid uint64) Writer { + return chunkForWrite(chunkid, s) +} + +func (s *cachedStore) Remove(chunkid uint64, length int) error { + r := chunkForRead(chunkid, length, s) + return r.Remove() +} + +var _ ChunkStore = &cachedStore{} diff --git a/pkg/chunk/cached_store_test.go b/pkg/chunk/cached_store_test.go new file mode 100644 index 000000000000..9cbf5dc392a3 --- /dev/null +++ b/pkg/chunk/cached_store_test.go @@ -0,0 +1,66 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "context" + "testing" + "time" + + "github.com/juicedata/juicesync/object" +) + +func BenchmarkCachedRead(b *testing.B) { + blob, _ := object.CreateStorage("mem", "", "", "") + config := defaultConf + config.BlockSize = 4 << 20 + store := NewCachedStore(blob, config) + w := store.NewWriter(1) + w.WriteAt(make([]byte, 1024), 0) + if err := w.Finish(1024); err != nil { + b.Fatalf("write fail: %s", err) + } + time.Sleep(time.Millisecond * 100) + p := NewPage(make([]byte, 1024)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + r := store.NewReader(1, 1024) + if n, err := r.ReadAt(context.Background(), p, 0); err != nil || n != 1024 { + b.FailNow() + } + } +} + +func BenchmarkUncachedRead(b *testing.B) { + blob, _ := object.CreateStorage("mem", "", "", "") + config := defaultConf + config.BlockSize = 4 << 20 + config.CacheSize = 0 + store := NewCachedStore(blob, config) + w := store.NewWriter(2) + w.WriteAt(make([]byte, 1024), 0) + if err := w.Finish(1024); err != nil { + b.Fatalf("write fail: %s", err) + } + p := NewPage(make([]byte, 1024)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + r := store.NewReader(2, 1024) + if n, err := r.ReadAt(context.Background(), p, 0); err != nil || n != 1024 { + b.FailNow() + } + } +} diff --git a/pkg/chunk/chunk.go b/pkg/chunk/chunk.go new file mode 100644 index 000000000000..20fa24141240 --- /dev/null +++ b/pkg/chunk/chunk.go @@ -0,0 +1,40 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "context" + "io" +) + +type Reader interface { + ReadAt(ctx context.Context, p *Page, off int) (int, error) +} + +type Writer interface { + io.WriterAt + ID() uint64 + SetID(chunkid uint64) + FlushTo(offset int) error + Finish(length int) error + Abort() +} + +type ChunkStore interface { + NewReader(chunkid uint64, length int) Reader + NewWriter(chunkid uint64) Writer + Remove(chunkid uint64, length int) error +} diff --git a/pkg/chunk/disk_cache.go b/pkg/chunk/disk_cache.go new file mode 100644 index 000000000000..31aa02b3e410 --- /dev/null +++ b/pkg/chunk/disk_cache.go @@ -0,0 +1,601 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "errors" + "hash/fnv" + "io" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + "sync" + "time" +) + +var ( + stagingDir = "rawstaging" + cacheDir = "raw" +) + +type cacheItem struct { + size int32 + atime uint32 +} + +type pendingFile struct { + key string + page *Page +} + +type cacheStore struct { + sync.Mutex + dir string + mode os.FileMode + capacity int64 + freeRatio float32 + limit int + pending chan pendingFile + pages map[string]*Page + + used int64 + keys map[string]cacheItem + scanned bool +} + +func newCacheStore(dir string, cacheSize int64, limit, pendingPages int, config *Config) *cacheStore { + if config.CacheMode == 0 { + config.CacheMode = 0600 // only owner can read/write cache + } + if config.FreeSpace == 0.0 { + config.FreeSpace = 0.1 // 10% + } + c := &cacheStore{ + dir: dir, + mode: config.CacheMode, + capacity: cacheSize, + freeRatio: config.FreeSpace, + limit: limit, + keys: make(map[string]cacheItem), + pending: make(chan pendingFile, pendingPages), + pages: make(map[string]*Page), + } + c.createDir(c.dir) + br, fr := c.curFreeRatio() + if br < c.freeRatio || fr < c.freeRatio { + logger.Warnf("not enough space (%d%%) or inodes (%d%%) for caching: free ratio should be >= %d%%", int(br*100), int(fr*100), int(c.freeRatio*100)) + } + go c.checkFreeSpace() + go c.refreshCacheKeys() + return c +} + +func (c *cacheStore) stats() (int64, int64) { + c.Lock() + defer c.Unlock() + var pendingBytes int64 + for _, p := range c.pages { + pendingBytes += int64(len(p.Data)) + } + return int64(len(c.pages) + len(c.keys)), c.used + pendingBytes +} + +func (c *cacheStore) checkFreeSpace() { + for { + br, fr := c.curFreeRatio() + if br < c.freeRatio || fr < c.freeRatio { + c.Lock() + c.cleanup() + c.Unlock() + } + time.Sleep(time.Second) + } +} + +func (c *cacheStore) refreshCacheKeys() { + for { + c.scanCached() + time.Sleep(time.Minute * 5) + } +} + +func (cache *cacheStore) cache(key string, p *Page) { + if cache.capacity == 0 { + return + } + cache.Lock() + defer cache.Unlock() + if _, ok := cache.pages[key]; ok { + return + } + p.Acquire() + cache.pages[key] = p + select { + case cache.pending <- pendingFile{key, p}: + default: + // does not have enough bandwidth to write it into disk, discard it + logger.Debugf("Caching queue is full, drop %s (%d bytes)", key, len(p.Data)) + delete(cache.pages, key) + p.Release() + } +} + +func (cache *cacheStore) curFreeRatio() (float32, float32) { + total, free, files, ffree := getDiskUsage(cache.dir) + return float32(free) / float32(total), float32(ffree) / float32(files) +} + +func (cache *cacheStore) flushPage(path string, data []byte) error { + cache.createDir(filepath.Dir(path)) + tmp := path + ".tmp" + f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE, cache.mode) + if err != nil { + logger.Infof("Can't create cache file %s: %s", tmp, err) + return err + } + _, err = f.Write(data) + if err != nil { + logger.Infof("Write to cache file %s: %s", tmp, err) + f.Close() + os.Remove(tmp) + return err + } + err = f.Close() + if err != nil { + logger.Infof("Close cache file %s: %s", tmp, err) + os.Remove(tmp) + return err + } + err = os.Rename(tmp, path) + if err != nil { + logger.Infof("Rename cache file %s -> %s: %s", tmp, path, err) + os.Remove(tmp) + } + return err +} + +func (cache *cacheStore) createDir(dir string) { + // who can read the cache, should be able to access the directories and add new file. + readmode := cache.mode & 0444 + mode := cache.mode | (readmode >> 2) | (readmode >> 1) + if st, err := os.Stat(dir); os.IsNotExist(err) { + if filepath.Dir(dir) != dir { + cache.createDir(filepath.Dir(dir)) + } + os.Mkdir(dir, mode) + // umask may remove some permisssions + os.Chmod(dir, mode) + } else if strings.HasPrefix(dir, cache.dir) && err == nil && st.Mode() != mode { + changeMode(dir, st, mode) + } +} + +func (cache *cacheStore) remove(key string) { + cache.Lock() + path := cache.cachePath(key) + if cache.keys[key].atime > 0 { + cache.used -= int64(cache.keys[key].size + 4096) + delete(cache.keys, key) + } else if cache.scanned { + path = "" // not existed + } + cache.Unlock() + if path != "" { + os.Remove(path) + os.Remove(cache.stagePath(key)) + } +} + +func (cache *cacheStore) load(key string) (ReadCloser, error) { + cache.Lock() + defer cache.Unlock() + if p, ok := cache.pages[key]; ok { + return NewPageReader(p), nil + } + if cache.scanned && cache.keys[key].atime == 0 { + return nil, errors.New("not cached") + } + cache.Unlock() + f, err := os.Open(cache.cachePath(key)) + cache.Lock() + if err == nil { + if it, ok := cache.keys[key]; ok { + // update atime + cache.keys[key] = cacheItem{it.size, uint32(time.Now().Unix())} + } + } + return f, err +} + +func (cache *cacheStore) cachePath(key string) string { + return filepath.Join(cache.dir, cacheDir, key) +} + +func (cache *cacheStore) stagePath(key string) string { + return filepath.Join(cache.dir, stagingDir, key) +} + +// flush cached block into disk +func (cache *cacheStore) flush() { + for { + w := <-cache.pending + path := cache.cachePath(w.key) + if cache.capacity > 0 && cache.flushPage(path, w.page.Data) == nil { + cache.add(w.key, int32(len(w.page.Data)), uint32(time.Now().Unix())) + } + cache.Lock() + delete(cache.pages, w.key) + cache.Unlock() + w.page.Release() + } +} + +func (cache *cacheStore) add(key string, size int32, atime uint32) { + cache.Lock() + defer cache.Unlock() + it, ok := cache.keys[key] + if ok { + cache.used -= int64(it.size + 4096) + } + if atime == 0 { + // update size of staging block + cache.keys[key] = cacheItem{size, it.atime} + } else { + cache.keys[key] = cacheItem{size, atime} + } + cache.used += int64(size + 4096) + + if cache.used > cache.capacity || len(cache.keys) > cache.limit { + cache.cleanup() + } +} + +func (c *cacheStore) stage(key string, data []byte, keepCache bool) (string, error) { + stagingPath := c.stagePath(key) + err := c.flushPage(stagingPath, data) + if err == nil && c.capacity > 0 && keepCache { + path := c.cachePath(key) + c.createDir(filepath.Dir(path)) + if err := os.Link(stagingPath, path); err == nil { + c.add(key, 0, uint32(time.Now().Unix())) + } else { + logger.Warnf("link %s to %s: %s", stagingPath, path, err) + } + } + return stagingPath, err +} + +func (c *cacheStore) uploaded(key string, size int) { + c.add(key, int32(size), 0) +} + +// locked +func (cache *cacheStore) cleanup() { + if !cache.scanned { + return + } + goal := cache.capacity * 95 / 100 + num := int(cache.limit * 95 / 100) + // make sure we have enough free space after cleanup + br, fr := cache.curFreeRatio() + if br < cache.freeRatio { + total, _, _, _ := getDiskUsage(cache.dir) + toFree := int64(float32(total) * (cache.freeRatio - br)) + if toFree > cache.used { + goal = 0 + } else if cache.used-toFree < goal { + goal = cache.used - toFree + } + } + if fr < cache.freeRatio { + _, _, files, _ := getDiskUsage(cache.dir) + toFree := int(float32(files) * (cache.freeRatio - fr)) + if toFree > len(cache.keys) { + num = 0 + } else { + num = len(cache.keys) - toFree + } + } + + var todel []string + var freed int64 + var cnt int + var lastKey string + var lastValue cacheItem + var now = uint32(time.Now().Unix()) + // for each two random keys, then compare the access time, evict the older one + for key, value := range cache.keys { + if value.size == 0 { + continue // staging + } + if cnt == 0 || lastValue.atime > value.atime { + lastKey = key + lastValue = value + } + cnt++ + if cnt > 1 { + delete(cache.keys, lastKey) + freed += int64(lastValue.size + 4096) + cache.used -= int64(lastValue.size + 4096) + todel = append(todel, lastKey) + logger.Debugf("remove %s from cache, age: %d", lastKey, now-lastValue.atime) + cnt = 0 + if len(cache.keys) < num && cache.used < goal { + break + } + } + } + if len(todel) > 0 { + logger.Debugf("cleanup cache: %d blocks (%d MB), freed %d blocks (%d MB)", len(cache.keys), cache.used>>20, len(todel), freed>>20) + } + cache.Unlock() + for _, key := range todel { + os.Remove(cache.cachePath(key)) + } + cache.Lock() +} + +func (c *cacheStore) scanCached() { + c.Lock() + c.used = 0 + c.keys = make(map[string]cacheItem) + c.scanned = false + c.Unlock() + + var start = time.Now() + var oneMinAgo = start.Add(-time.Minute) + + cachePrefix := filepath.Join(c.dir, cacheDir) + logger.Debugf("Scan %s to find cached blocks", cachePrefix) + filepath.Walk(cachePrefix, func(path string, fi os.FileInfo, err error) error { + if fi != nil { + if fi.IsDir() || strings.HasSuffix(path, ".tmp") { + if fi.ModTime().Before(oneMinAgo) { + // try to remove empty directory + if os.Remove(path) == nil { + logger.Debugf("Remove empty directory: %s", path) + } + } + } else { + key := path[len(cachePrefix)+1:] + if runtime.GOOS == "windows" { + key = strings.ReplaceAll(key, "\\", "/") + } + atime := uint32(getAtime(fi).Unix()) + if getNlink(fi) > 1 { + c.add(key, 0, atime) + } else { + c.add(key, int32(fi.Size()), atime) + } + } + } + return nil + }) + + c.Lock() + c.scanned = true + logger.Debugf("Found %d cached blocks (%d bytes) in %s", len(c.keys), c.used, time.Since(start)) + c.Unlock() +} + +func (cache *cacheStore) scanStaging() map[string]string { + var start = time.Now() + var oneMinAgo = start.Add(-time.Minute) + + stagingBlocks := make(map[string]string) + stagingPrefix := filepath.Join(cache.dir, stagingDir) + logger.Debugf("Scan %s to find staging blocks", stagingPrefix) + filepath.Walk(stagingPrefix, func(path string, fi os.FileInfo, err error) error { + if fi != nil { + if fi.IsDir() || strings.HasSuffix(path, ".tmp") { + if fi.ModTime().Before(oneMinAgo) { + // try to remove empty directory + if os.Remove(path) == nil { + logger.Debugf("Remove empty directory: %s", path) + } + } + } else { + logger.Debugf("Found staging block: %s", path) + key := path[len(stagingPrefix)+1:] + if runtime.GOOS == "windows" { + key = strings.ReplaceAll(key, "\\", "/") + } + stagingBlocks[key] = path + } + } + return nil + }) + if len(stagingBlocks) > 0 { + logger.Infof("Found %d staging blocks (%d bytes) in %s", len(stagingBlocks), cache.used, time.Since(start)) + } + return stagingBlocks +} + +type cacheManager struct { + stores []*cacheStore +} + +func keyHash(s string) uint32 { + hash := fnv.New32() + hash.Write([]byte(s)) + return hash.Sum32() +} + +func splitDir(d string) []string { + dd := strings.Split(d, string(os.PathListSeparator)) + if len(dd) == 1 { + dd = strings.Split(dd[0], ",") + } + return dd +} + +// hasMeta reports whether path contains any of the magic characters +// recognized by Match. +func hasMeta(path string) bool { + magicChars := `*?[` + if runtime.GOOS != "windows" { + magicChars = `*?[\` + } + return strings.ContainsAny(path, magicChars) +} + +func expandDir(pattern string) []string { + for strings.HasSuffix(pattern, "/") { + pattern = pattern[:len(pattern)-1] + } + if pattern == "" { + return []string{"/"} + } + if !hasMeta(pattern) { + return []string{pattern} + } + dir, f := filepath.Split(pattern) + if hasMeta(f) { + matched, err := filepath.Glob(pattern) + if err != nil { + logger.Errorf("glob %s: %s", pattern, err) + return []string{pattern} + } + return matched + } + var rs []string + for _, p := range expandDir(dir) { + rs = append(rs, filepath.Join(p, f)) + } + return rs +} + +type CacheManager interface { + cache(key string, p *Page) + remove(key string) + load(key string) (ReadCloser, error) + uploaded(key string, size int) + stage(key string, data []byte, keepCache bool) (string, error) + scanStaging() map[string]string + stats() (int64, int64) +} + +func newCacheManager(config *Config) CacheManager { + logger.Infof("Cache: %s capacity: %d MB", config.CacheDir, config.CacheSize) + if config.CacheDir == "memory" || config.CacheSize == 0 { + return newMemStore(config) + } + var dirs []string + for _, d := range splitDir(config.CacheDir) { + dd := expandDir(d) + if config.AutoCreate { + dirs = append(dirs, dd...) + } else { + for _, d := range dd { + if fi, err := os.Stat(d); err == nil && fi.IsDir() { + dirs = append(dirs, d) + } + } + } + } + if len(dirs) == 0 { + logger.Warnf("No cache dir existed") + return newMemStore(config) + } + sort.Strings(dirs) + dirCacheSize := config.CacheSize << 20 + dirCacheSize /= int64(len(dirs)) + limit := dirCacheSize / int64(config.BlockSize) * 2 + if limit < 1000000 { + limit = 1000000 + } + m := &cacheManager{ + stores: make([]*cacheStore, len(dirs)), + } + // 20% of buffer could be used for pending pages + pendingPages := config.BufferSize * 2 / 10 / config.BlockSize / len(dirs) + for i, d := range dirs { + m.stores[i] = newCacheStore(strings.TrimSpace(d)+string(filepath.Separator), dirCacheSize, int(limit), pendingPages, config) + } + return m +} + +func (m *cacheManager) getStore(key string) *cacheStore { + return m.stores[keyHash(key)%uint32(len(m.stores))] +} + +func (m *cacheManager) stats() (int64, int64) { + var cnt, used int64 + for _, s := range m.stores { + c, u := s.stats() + cnt += c + used += u + } + return cnt, used +} + +func (m *cacheManager) cache(key string, p *Page) { + if len(m.stores) == 0 { + return + } + m.getStore(key).cache(key, p) +} + +type ReadCloser interface { + io.Reader + io.ReaderAt + io.Closer +} + +func (m *cacheManager) load(key string) (ReadCloser, error) { + if len(m.stores) == 0 { + return nil, errors.New("no cache dir") + } + return m.getStore(key).load(key) +} + +func (m *cacheManager) remove(key string) { + if len(m.stores) > 0 { + m.getStore(key).remove(key) + } +} + +func (m *cacheManager) stage(key string, data []byte, keepCache bool) (string, error) { + if len(m.stores) == 0 { + return "", errors.New("no cache dir") + } + return m.getStore(key).stage(key, data, keepCache) +} + +func (m *cacheManager) uploaded(key string, size int) { + if len(m.stores) > 0 { + m.getStore(key).uploaded(key, size) + } +} + +func (m *cacheManager) scanStaging() map[string]string { + fschan := make(chan map[string]string) + for i := range m.stores { + go func(i int) { + fschan <- m.stores[i].scanStaging() + }(i) + } + files := make(map[string]string) + for range m.stores { + fs := <-fschan + for k, p := range fs { + files[k] = p + } + } + return files +} diff --git a/pkg/chunk/disk_cache_test.go b/pkg/chunk/disk_cache_test.go new file mode 100644 index 000000000000..85d55ed41636 --- /dev/null +++ b/pkg/chunk/disk_cache_test.go @@ -0,0 +1,69 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "os" + "testing" + "time" +) + +func TestExpand(t *testing.T) { + rs := expandDir("/not/exists/jfsCache") + if len(rs) != 1 || rs[0] != "/not/exists/jfsCache" { + t.Errorf("expand: %v", rs) + t.FailNow() + } + + os.Mkdir("/tmp/aaa1", 0755) + os.Mkdir("/tmp/aaa2", 0755) + os.Mkdir("/tmp/aaa3", 0755) + os.Mkdir("/tmp/aaa3/jfscache", 0755) + os.Mkdir("/tmp/aaa3/jfscache/jfs", 0755) + + rs = expandDir("/tmp/aaa*/jfscache/jfs") + if len(rs) != 3 || rs[0] != "/tmp/aaa1/jfscache/jfs" { + t.Errorf("expand: %v", rs) + t.FailNow() + } +} + +func BenchmarkLoadCached(b *testing.B) { + s := newCacheStore("/tmp/diskCache", 1<<30, 1<<10, 1, &defaultConf) + p := NewPage(make([]byte, 1024)) + key := "/chunks/1_1024" + s.cache(key, p) + time.Sleep(time.Millisecond * 100) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if f, e := s.load(key); e == nil { + f.Close() + } else { + b.FailNow() + } + } +} + +func BenchmarkLoadUncached(b *testing.B) { + s := newCacheStore("/tmp/diskCache", 1<<30, 1<<10, 1, &defaultConf) + key := "/chunks/222_1024" + b.ResetTimer() + for i := 0; i < b.N; i++ { + if f, e := s.load(key); e != nil { + f.Close() + } + } +} diff --git a/pkg/chunk/disk_store.go b/pkg/chunk/disk_store.go new file mode 100644 index 000000000000..21f605459a08 --- /dev/null +++ b/pkg/chunk/disk_store.go @@ -0,0 +1,111 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "context" + "fmt" + "os" + "path/filepath" +) + +type diskFile struct { + id uint64 + path string +} + +func (c *diskFile) ID() uint64 { + return c.id +} + +func (c *diskFile) SetID(id uint64) { + c.id = id +} + +func (c *diskFile) ReadAt(ctx context.Context, p *Page, off int) (n int, err error) { + f, err := os.Open(c.path) + if err != nil { + return 0, err + } + st, _ := f.Stat() + defer f.Close() + if len(p.Data) > int(st.Size())-off { + return f.ReadAt(p.Data[:st.Size()-int64(off)], int64(off)) + } + return f.ReadAt(p.Data, int64(off)) +} + +func (c *diskFile) WriteAt(p []byte, off int64) (n int, err error) { + f, err := os.OpenFile(c.path, os.O_CREATE|os.O_WRONLY, os.FileMode(0644)) + if err != nil { + return 0, err + } + defer f.Close() + return f.WriteAt(p, off) +} + +func (c *diskFile) FlushTo(offset int) error { + return nil +} + +func (c *diskFile) Len() int { + fi, err := os.Stat(c.path) + if err != nil { + return 0 + } + return int(fi.Size()) +} + +func (c *diskFile) Finish(length int) error { + if c.Len() < length { + return fmt.Errorf("data length mismatch: %v != %v", c.Len(), length) + } + return nil +} + +func (c *diskFile) Abort() { + os.Remove(c.path) +} + +type diskStore struct { + root string +} + +func (s *diskStore) chunkPath(chunkid uint64) string { + name := fmt.Sprintf("%v.chunk", chunkid) + return filepath.Join(s.root, name) +} + +func NewDiskStore(dir string) ChunkStore { + if _, err := os.Stat(dir); os.IsNotExist(err) { + os.Mkdir(dir, 0755) + } + return &diskStore{dir} +} + +func (s *diskStore) NewReader(chunkid uint64, length int) Reader { + return &diskFile{chunkid, s.chunkPath(chunkid)} +} + +func (s *diskStore) NewWriter(chunkid uint64) Writer { + return &diskFile{chunkid, s.chunkPath(chunkid)} +} + +func (s *diskStore) Remove(chunkid uint64, length int) error { + return os.Remove(s.chunkPath(chunkid)) +} + +var _ ChunkStore = &diskStore{} diff --git a/pkg/chunk/mem_cache.go b/pkg/chunk/mem_cache.go new file mode 100644 index 000000000000..71104f7af926 --- /dev/null +++ b/pkg/chunk/mem_cache.go @@ -0,0 +1,122 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "errors" + "sync" + "time" +) + +type memItem struct { + atime time.Time + page *Page +} + +type memcache struct { + sync.Mutex + capacity int64 + used int64 + pages map[string]memItem +} + +func newMemStore(config *Config) *memcache { + c := &memcache{ + capacity: config.CacheSize << 20, + pages: make(map[string]memItem), + } + return c +} + +func (c *memcache) stats() (int64, int64) { + c.Lock() + defer c.Unlock() + return int64(len(c.pages)), c.used +} + +func (c *memcache) cache(key string, p *Page) { + if c.capacity == 0 { + return + } + c.Lock() + defer c.Unlock() + if _, ok := c.pages[key]; ok { + return + } + p.Acquire() + size := int64(cap(p.Data)) + c.pages[key] = memItem{time.Now(), p} + c.used += size + 4096 + if c.used > c.capacity { + c.cleanup() + } +} + +func (c *memcache) delete(key string, p *Page) { + size := int64(cap(p.Data)) + c.used -= size + 4096 + p.Release() + delete(c.pages, key) +} + +func (c *memcache) remove(key string) { + c.Lock() + defer c.Unlock() + if item, ok := c.pages[key]; ok { + c.delete(key, item.page) + logger.Debugf("remove %s from cache", key) + } +} + +func (c *memcache) load(key string) (ReadCloser, error) { + c.Lock() + defer c.Unlock() + if item, ok := c.pages[key]; ok { + c.pages[key] = memItem{time.Now(), item.page} + return NewPageReader(item.page), nil + } + return nil, errors.New("not found") +} + +// locked +func (c *memcache) cleanup() { + var cnt int + var lastKey string + var lastValue memItem + var now = time.Now() + // for each two random keys, then compare the access time, evict the older one + for k, v := range c.pages { + if cnt == 0 || lastValue.atime.After(v.atime) { + lastKey = k + lastValue = v + } + cnt++ + if cnt > 1 { + logger.Debugf("remove %s from cache, age: %d", lastKey, now.Sub(lastValue.atime)) + c.delete(lastKey, lastValue.page) + cnt = 0 + if c.used < c.capacity { + break + } + } + } +} + +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) scanStaging() map[string]string { return nil } diff --git a/pkg/chunk/page.go b/pkg/chunk/page.go new file mode 100644 index 000000000000..f003a6c1407e --- /dev/null +++ b/pkg/chunk/page.go @@ -0,0 +1,133 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "errors" + "io" + "runtime" + "sync/atomic" + + "github.com/juicedata/juicefs/pkg/utils" +) + +// Page is a page with refcount +type Page struct { + refs int32 + offheap bool + dep *Page + Data []byte +} + +// NewPage create a new page. +func NewPage(data []byte) *Page { + return &Page{refs: 1, Data: data} +} + +func NewOffPage(size int) *Page { + if size <= 0 { + panic("size of page should > 0") + } + p := utils.Alloc(size) + page := &Page{refs: 1, offheap: true, Data: p} + runtime.SetFinalizer(page, func(p *Page) { + refcnt := atomic.LoadInt32(&p.refs) + if refcnt != 0 { + logger.Errorf("refcount of page %p is not zero: %d", p, refcnt) + if refcnt > 0 { + p.Release() + } + } + }) + return page +} + +func (p *Page) Slice(off, len int) *Page { + p.Acquire() + np := NewPage(p.Data[off : off+len]) + np.dep = p + return np +} + +func (p *Page) isOffHeap() bool { + if p.offheap { + return true + } + if p.dep != nil { + return p.dep.isOffHeap() + } + return false +} + +// Acquire increase the refcount +func (p *Page) Acquire() { + atomic.AddInt32(&p.refs, 1) +} + +// Release decrease the refcount +func (p *Page) Release() { + if atomic.AddInt32(&p.refs, -1) == 0 { + if p.offheap { + utils.Free(p.Data) + } + if p.dep != nil { + p.dep.Release() + p.dep = nil + } + p.Data = nil + } +} + +type pageReader struct { + p *Page + off int +} + +func NewPageReader(p *Page) *pageReader { + p.Acquire() + return &pageReader{p, 0} +} + +func (r *pageReader) Read(buf []byte) (int, error) { + n, err := r.ReadAt(buf, int64(r.off)) + r.off += n + return n, err +} + +func (r *pageReader) ReadAt(buf []byte, off int64) (int, error) { + if len(buf) == 0 { + return 0, nil + } + if r.p == nil { + return 0, errors.New("page is already released") + } + if int(off) == len(r.p.Data) { + return 0, io.EOF + } + n := copy(buf, r.p.Data[off:]) + if n < len(buf) { + return n, io.EOF + } + return n, nil +} + +func (r *pageReader) Close() error { + if r.p != nil { + r.p.Release() + r.p = nil + } + return nil +} diff --git a/pkg/chunk/page_test.go b/pkg/chunk/page_test.go new file mode 100644 index 000000000000..05f49e876001 --- /dev/null +++ b/pkg/chunk/page_test.go @@ -0,0 +1,80 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "io" + "testing" +) + +func TestPage(t *testing.T) { + p1 := NewOffPage(1) + if len(p1.Data) != 1 { + t.Fail() + } + if cap(p1.Data) != 1 { + t.Fail() + } + p1.Acquire() + p1.Release() + if p1.Data == nil { + t.Fail() + } + + p2 := p1.Slice(0, 1) + p1.Release() + if p1.Data == nil { + t.Fail() + } + + p2.Release() + if p2.Data != nil { + t.Fail() + } + if p1.Data != nil { + t.Fail() + } +} + +func TestPageReader(t *testing.T) { + data := []byte("hello") + p := NewPage(data) + r := NewPageReader(p) + + if n, err := r.Read(nil); n != 0 || err != nil { + t.Fatalf("read should return 0") + } + buf := make([]byte, 3) + if n, err := r.Read(buf); n != 3 || err != nil { + t.Fatalf("read should return 3 but got %d", n) + } + if n, err := r.Read(buf); n != 2 || (err != nil && err != io.EOF) { + t.Fatalf("read should return 2 but got %d", n) + } + if n, err := r.Read(buf); n != 0 || err != io.EOF { + t.Fatalf("read should return 0") + } + if n, err := r.ReadAt(buf, 4); n != 1 || (err != nil && err != io.EOF) { + t.Fatalf("read should return 1") + } + if n, err := r.ReadAt(buf, 5); n != 0 || err != io.EOF { + t.Fatalf("read should return 0") + } + r.Close() + if n, err := r.ReadAt(buf, 5); n != 0 || err == nil { + t.Fatalf("read should fail after close") + } +} diff --git a/pkg/chunk/prefetch.go b/pkg/chunk/prefetch.go new file mode 100644 index 000000000000..2fa7c5cf99de --- /dev/null +++ b/pkg/chunk/prefetch.go @@ -0,0 +1,60 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import "sync" + +type prefetcher struct { + sync.Mutex + pending chan string + busy map[string]bool + op func(key string) +} + +func newPrefetcher(parallel int, fetch func(string)) *prefetcher { + p := &prefetcher{ + pending: make(chan string, 10), + busy: make(map[string]bool), + op: fetch, + } + for i := 0; i < parallel; i++ { + go p.do() + } + return p +} + +func (p *prefetcher) do() { + for key := range p.pending { + p.Lock() + if _, ok := p.busy[key]; !ok { + p.busy[key] = true + p.Unlock() + + p.op(key) + + p.Lock() + delete(p.busy, key) + } + p.Unlock() + } +} + +func (p *prefetcher) fetch(key string) { + select { + case p.pending <- key: + default: + } +} diff --git a/pkg/chunk/singleflight.go b/pkg/chunk/singleflight.go new file mode 100644 index 000000000000..6fb6f5d89a5d --- /dev/null +++ b/pkg/chunk/singleflight.go @@ -0,0 +1,69 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import "sync" + +type request struct { + wg sync.WaitGroup + val *Page + ref int + err error +} + +type Controller struct { + sync.Mutex + rs map[string]*request +} + +func (con *Controller) Execute(key string, fn func() (*Page, error)) (*Page, error) { + con.Lock() + if con.rs == nil { + con.rs = make(map[string]*request) + } + if c, ok := con.rs[key]; ok { + c.ref++ + con.Unlock() + c.wg.Wait() + c.val.Acquire() + con.Lock() + c.ref-- + if c.ref == 0 { + c.val.Release() + } + con.Unlock() + return c.val, c.err + } + c := new(request) + c.wg.Add(1) + c.ref++ + con.rs[key] = c + con.Unlock() + + c.val, c.err = fn() + c.val.Acquire() + c.wg.Done() + + con.Lock() + c.ref-- + if c.ref == 0 { + c.val.Release() + } + delete(con.rs, key) + con.Unlock() + + return c.val, c.err +} diff --git a/pkg/chunk/singleflight_test.go b/pkg/chunk/singleflight_test.go new file mode 100644 index 000000000000..994250eb26a2 --- /dev/null +++ b/pkg/chunk/singleflight_test.go @@ -0,0 +1,40 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "strconv" + "sync" + "testing" + "time" +) + +func TestSigleFlight(t *testing.T) { + g := &Controller{} + gp := &sync.WaitGroup{} + for i := 0; i < 100000; i++ { + gp.Add(1) + go func(k int) { + p, _ := g.Execute(strconv.Itoa(k/1000), func() (*Page, error) { + time.Sleep(time.Microsecond * 1000) + return NewOffPage(100), nil + }) + p.Release() + gp.Done() + }(i) + } + gp.Wait() +} diff --git a/pkg/chunk/store_test.go b/pkg/chunk/store_test.go new file mode 100644 index 000000000000..0966b08195a4 --- /dev/null +++ b/pkg/chunk/store_test.go @@ -0,0 +1,110 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "context" + "io" + "os" + "path/filepath" + "testing" + "time" + + "github.com/juicedata/juicefs/pkg/utils" + "github.com/juicedata/juicesync/object" + "github.com/sirupsen/logrus" +) + +func testStore(t *testing.T, store ChunkStore) { + utils.SetLogLevel(logrus.DebugLevel) + writer := store.NewWriter(1) + data := []byte("hello world") + if n, err := writer.WriteAt(data, 0); n != 11 || err != nil { + t.Fatalf("write fail: %d %s", n, err) + } + offset := defaultConf.BlockSize - 3 + if n, err := writer.WriteAt(data, int64(offset)); err != nil || n != 11 { + t.Fatalf("write fail: %d %s", n, err) + } + if err := writer.FlushTo(defaultConf.BlockSize + 3); err != nil { + t.Fatalf("flush fail: %s", err) + } + size := offset + len(data) + if err := writer.Finish(size); err != nil { + t.Fatalf("finish fail: %s", err) + } + defer store.Remove(1, size) + + reader := store.NewReader(1, size) + p := NewPage(make([]byte, 5)) + if n, err := reader.ReadAt(context.Background(), p, 6); n != 5 || err != nil { + t.Fatalf("read failed: %d %s", n, err) + } else if string(p.Data[:n]) != "world" { + t.Fatalf("not expected: %s", string(p.Data[:n])) + } + p = NewPage(make([]byte, 20)) + if n, err := reader.ReadAt(context.Background(), p, offset); n != 11 || err != nil && err != io.EOF { + t.Fatalf("read failed: %d %s", n, err) + } else if string(p.Data[:n]) != "hello world" { + t.Fatalf("not expected: %s", string(p.Data[:n])) + } +} + +func TestDiskStore(t *testing.T) { + testStore(t, NewDiskStore("/tmp/diskStore")) +} + +var defaultConf = Config{ + BlockSize: 1024, + CacheDir: "/tmp/diskCache", + CacheSize: 10, + MaxUpload: 1, + PutTimeout: time.Second, + GetTimeout: time.Second * 2, +} + +func TestCachedStore(t *testing.T) { + mem, _ := object.CreateStorage("mem", "", "", "") + store := NewCachedStore(mem, defaultConf) + testStore(t, store) +} + +func TestUncompressedStore(t *testing.T) { + mem, _ := object.CreateStorage("mem", "", "", "") + conf := defaultConf + conf.Compress = "" + conf.CacheSize = 0 + store := NewCachedStore(mem, conf) + testStore(t, store) +} + +func TestAsyncStore(t *testing.T) { + mem, _ := object.CreateStorage("mem", "", "", "") + conf := defaultConf + conf.CacheDir = "/tmp/testdirAsync" + p := filepath.Join(conf.CacheDir, stagingDir, "chunks/0/0/123_0") + os.MkdirAll(filepath.Dir(p), 0744) + f, _ := os.Create(p) + f.WriteString("good") + f.Close() + conf.AsyncUpload = true + conf.UploadLimit = 0 + _ = NewCachedStore(mem, conf) + time.Sleep(time.Millisecond * 10) // wait for scan to finish + if _, err := mem.Head("chunks/0/0/123_0_4"); err != nil { + t.Fatalf("staging object should be upload") + } +} diff --git a/pkg/chunk/utils_darwin.go b/pkg/chunk/utils_darwin.go new file mode 100644 index 000000000000..62fef3d406a6 --- /dev/null +++ b/pkg/chunk/utils_darwin.go @@ -0,0 +1,30 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "os" + "syscall" + "time" +) + +func getAtime(fi os.FileInfo) time.Time { + if sst, ok := fi.Sys().(*syscall.Stat_t); ok { + return time.Unix(sst.Atimespec.Unix()) + } else { + return fi.ModTime() + } +} diff --git a/pkg/chunk/utils_linux.go b/pkg/chunk/utils_linux.go new file mode 100644 index 000000000000..8554e3a07973 --- /dev/null +++ b/pkg/chunk/utils_linux.go @@ -0,0 +1,30 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "os" + "syscall" + "time" +) + +func getAtime(fi os.FileInfo) time.Time { + if sst, ok := fi.Sys().(*syscall.Stat_t); ok { + return time.Unix(sst.Atim.Unix()) + } else { + return fi.ModTime() + } +} diff --git a/pkg/chunk/utils_unix.go b/pkg/chunk/utils_unix.go new file mode 100644 index 000000000000..1846a29b1eba --- /dev/null +++ b/pkg/chunk/utils_unix.go @@ -0,0 +1,47 @@ +// +build !windows + +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package chunk + +import ( + "os" + "syscall" +) + +func getNlink(fi os.FileInfo) int { + if sst, ok := fi.Sys().(*syscall.Stat_t); ok { + return int(sst.Nlink) + } + return 1 +} + +func getDiskUsage(path string) (uint64, uint64, uint64, uint64) { + var stat syscall.Statfs_t + if err := syscall.Statfs(path, &stat); err == nil { + return stat.Blocks * uint64(stat.Bsize), stat.Bavail * uint64(stat.Bsize), uint64(stat.Files), uint64(stat.Ffree) + } else { + logger.Warnf("statfs %s: %s", path, err) + return 1, 1, 1, 1 + } +} + +func changeMode(dir string, st os.FileInfo, mode os.FileMode) { + sst := st.Sys().(*syscall.Stat_t) + if os.Getuid() == int(sst.Uid) { + os.Chmod(dir, mode) + } +} diff --git a/pkg/fuse/context.go b/pkg/fuse/context.go new file mode 100644 index 000000000000..a0e282ea5038 --- /dev/null +++ b/pkg/fuse/context.go @@ -0,0 +1,88 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package fuse + +import ( + "sync" + "time" + + "github.com/juicedata/juicefs/pkg/meta" + "github.com/juicedata/juicefs/pkg/vfs" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +type Ino = meta.Ino +type Attr = meta.Attr +type Context = vfs.LogContext + +type fuseContext struct { + start time.Time + header *fuse.InHeader + canceled bool + cancel <-chan struct{} +} + +var contextPool = sync.Pool{ + New: func() interface{} { + return &fuseContext{} + }, +} + +func newContext(cancel <-chan struct{}, header *fuse.InHeader) *fuseContext { + ctx := contextPool.Get().(*fuseContext) + ctx.start = time.Now() + ctx.canceled = false + ctx.cancel = cancel + ctx.header = header + return ctx +} + +func releaseContext(ctx *fuseContext) { + contextPool.Put(ctx) +} + +func (c *fuseContext) Uid() uint32 { + return uint32(c.header.Uid) +} + +func (c *fuseContext) Gid() uint32 { + return uint32(c.header.Gid) +} + +func (c *fuseContext) Pid() uint32 { + return uint32(c.header.Pid) +} + +func (c *fuseContext) Duration() time.Duration { + return time.Since(c.start) +} + +func (c *fuseContext) Cancel() { + c.canceled = true +} + +func (c *fuseContext) Canceled() bool { + if c.canceled { + return true + } + select { + case <-c.cancel: + return true + default: + return false + } +} diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go new file mode 100644 index 000000000000..1e3b61c26347 --- /dev/null +++ b/pkg/fuse/fuse.go @@ -0,0 +1,455 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package fuse + +import ( + "fmt" + "os" + "runtime" + "strings" + "syscall" + "time" + + "github.com/juicedata/juicefs/pkg/meta" + "github.com/juicedata/juicefs/pkg/utils" + "github.com/juicedata/juicefs/pkg/vfs" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +var logger = utils.GetLogger("juicefs") + +type JFS struct { + fuse.RawFileSystem + cacheMode int + attrTimeout time.Duration + direntryTimeout time.Duration + entryTimeout time.Duration +} + +func NewJFS() *JFS { + return &JFS{ + RawFileSystem: fuse.NewDefaultRawFileSystem(), + } +} + +func (fs *JFS) replyEntry(out *fuse.EntryOut, entry *meta.Entry) fuse.Status { + out.NodeId = uint64(entry.Inode) + out.Generation = 1 + out.SetAttrTimeout(fs.attrTimeout) + if entry.Attr.Typ == meta.TypeDirectory { + out.SetEntryTimeout(fs.direntryTimeout) + } else { + out.SetEntryTimeout(fs.entryTimeout) + } + if vfs.IsSpecialNode(entry.Inode) { + out.SetAttrTimeout(time.Hour) + } + attrToStat(entry.Inode, entry.Attr, &out.Attr) + return 0 +} + +func (fs *JFS) Lookup(cancel <-chan struct{}, header *fuse.InHeader, name string, out *fuse.EntryOut) (status fuse.Status) { + ctx := newContext(cancel, header) + defer releaseContext(ctx) + entry, err := vfs.Lookup(ctx, Ino(header.NodeId), name) + if err != 0 { + return fuse.Status(err) + } + return fs.replyEntry(out, entry) +} + +func (fs *JFS) GetAttr(cancel <-chan struct{}, in *fuse.GetAttrIn, out *fuse.AttrOut) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + var opened uint8 + if in.Fh() != 0 { + opened = 1 + } + entry, err := vfs.GetAttr(ctx, Ino(in.NodeId), opened) + if err != 0 { + return fuse.Status(err) + } + attrToStat(entry.Inode, entry.Attr, &out.Attr) + out.AttrValid = uint64(fs.attrTimeout.Seconds()) + if vfs.IsSpecialNode(Ino(in.NodeId)) { + out.AttrValid = 3600 + } + return 0 +} + +func (fs *JFS) SetAttr(cancel <-chan struct{}, in *fuse.SetAttrIn, out *fuse.AttrOut) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + var opened uint8 + if in.Fh != 0 { + opened = 1 + } + entry, err := vfs.SetAttr(ctx, Ino(in.NodeId), int(in.Valid), opened, in.Mode, in.Uid, in.Gid, int64(in.Atime), int64(in.Mtime), in.Atimensec, in.Mtimensec, in.Size) + if err != 0 { + return fuse.Status(err) + } + out.AttrValid = uint64(fs.attrTimeout.Seconds()) + if vfs.IsSpecialNode(entry.Inode) { + out.AttrValid = 3600 + } + attrToStat(entry.Inode, entry.Attr, &out.Attr) + return 0 +} + +func (fs *JFS) Mknod(cancel <-chan struct{}, in *fuse.MknodIn, name string, out *fuse.EntryOut) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + entry, err := vfs.Mknod(ctx, Ino(in.NodeId), name, uint16(in.Mode), getUmask(in), in.Rdev) + if err != 0 { + return fuse.Status(err) + } + return fs.replyEntry(out, entry) +} + +func (fs *JFS) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out *fuse.EntryOut) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + entry, err := vfs.Mkdir(ctx, Ino(in.NodeId), name, uint16(in.Mode), uint16(in.Umask)) + if err != 0 { + return fuse.Status(err) + } + return fs.replyEntry(out, entry) +} + +func (fs *JFS) Unlink(cancel <-chan struct{}, header *fuse.InHeader, name string) (code fuse.Status) { + ctx := newContext(cancel, header) + defer releaseContext(ctx) + err := vfs.Unlink(ctx, Ino(header.NodeId), name) + return fuse.Status(err) +} + +func (fs *JFS) Rmdir(cancel <-chan struct{}, header *fuse.InHeader, name string) (code fuse.Status) { + ctx := newContext(cancel, header) + defer releaseContext(ctx) + err := vfs.Rmdir(ctx, Ino(header.NodeId), name) + return fuse.Status(err) +} + +func (fs *JFS) Rename(cancel <-chan struct{}, in *fuse.RenameIn, oldName string, newName string) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + err := vfs.Rename(ctx, Ino(in.NodeId), oldName, Ino(in.Newdir), newName) + return fuse.Status(err) +} + +func (fs *JFS) Link(cancel <-chan struct{}, in *fuse.LinkIn, name string, out *fuse.EntryOut) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + entry, err := vfs.Link(ctx, Ino(in.Oldnodeid), Ino(in.NodeId), name) + if err != 0 { + return fuse.Status(err) + } + return fs.replyEntry(out, entry) +} + +func (fs *JFS) Symlink(cancel <-chan struct{}, header *fuse.InHeader, target string, name string, out *fuse.EntryOut) (code fuse.Status) { + ctx := newContext(cancel, header) + defer releaseContext(ctx) + entry, err := vfs.Symlink(ctx, target, Ino(header.NodeId), name) + if err != 0 { + return fuse.Status(err) + } + return fs.replyEntry(out, entry) +} + +func (fs *JFS) Readlink(cancel <-chan struct{}, header *fuse.InHeader) (out []byte, code fuse.Status) { + ctx := newContext(cancel, header) + defer releaseContext(ctx) + path, err := vfs.Readlink(ctx, Ino(header.NodeId)) + return path, fuse.Status(err) +} + +func (fs *JFS) GetXAttr(cancel <-chan struct{}, header *fuse.InHeader, attr string, dest []byte) (sz uint32, code fuse.Status) { + ctx := newContext(cancel, header) + defer releaseContext(ctx) + value, err := vfs.GetXattr(ctx, Ino(header.NodeId), attr, uint32(len(dest))) + if err != 0 { + return 0, fuse.Status(err) + } + copy(dest, value) + return uint32(len(value)), 0 +} + +func (fs *JFS) ListXAttr(cancel <-chan struct{}, header *fuse.InHeader, dest []byte) (uint32, fuse.Status) { + ctx := newContext(cancel, header) + defer releaseContext(ctx) + data, err := vfs.ListXattr(ctx, Ino(header.NodeId), len(dest)) + if err != 0 { + return 0, fuse.Status(err) + } + copy(dest, data) + return uint32(len(data)), 0 +} + +func (fs *JFS) SetXAttr(cancel <-chan struct{}, in *fuse.SetXAttrIn, attr string, data []byte) fuse.Status { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + err := vfs.SetXattr(ctx, Ino(in.NodeId), attr, data, int(in.Flags)) + return fuse.Status(err) +} + +func (fs *JFS) RemoveXAttr(cancel <-chan struct{}, header *fuse.InHeader, attr string) (code fuse.Status) { + ctx := newContext(cancel, header) + defer releaseContext(ctx) + err := vfs.RemoveXattr(ctx, Ino(header.NodeId), attr) + return fuse.Status(err) +} + +func (fs *JFS) Access(cancel <-chan struct{}, in *fuse.AccessIn) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + err := vfs.Access(ctx, Ino(in.NodeId), int(in.Mask)) + return fuse.Status(err) +} + +func (fs *JFS) Create(cancel <-chan struct{}, in *fuse.CreateIn, name string, out *fuse.CreateOut) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + entry, fh, err := vfs.Create(ctx, Ino(in.NodeId), name, uint16(in.Mode), 0, in.Flags) + if err != 0 { + return fuse.Status(err) + } + out.Fh = fh + return fs.replyEntry(&out.EntryOut, entry) +} + +func (fs *JFS) Open(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut) (status fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + _, fh, err := vfs.Open(ctx, Ino(in.NodeId), in.Flags) + if err != 0 { + return fuse.Status(err) + } + out.Fh = fh + if vfs.IsSpecialNode(Ino(in.NodeId)) { + out.OpenFlags |= fuse.FOPEN_DIRECT_IO + } + return 0 +} + +func (fs *JFS) Read(cancel <-chan struct{}, in *fuse.ReadIn, buf []byte) (fuse.ReadResult, fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + n, err := vfs.Read(ctx, Ino(in.NodeId), buf, in.Offset, in.Fh) + if err != 0 { + return nil, fuse.Status(err) + } + return fuse.ReadResultData(buf[:n]), 0 +} + +func (fs *JFS) Release(cancel <-chan struct{}, in *fuse.ReleaseIn) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + vfs.Release(ctx, Ino(in.NodeId), in.Fh) +} + +func (fs *JFS) Write(cancel <-chan struct{}, in *fuse.WriteIn, data []byte) (written uint32, code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + err := vfs.Write(ctx, Ino(in.NodeId), data, in.Offset, in.Fh) + if err != 0 { + return 0, fuse.Status(err) + } + return uint32(len(data)), 0 +} + +func (fs *JFS) Flush(cancel <-chan struct{}, in *fuse.FlushIn) fuse.Status { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + err := vfs.Flush(ctx, Ino(in.NodeId), in.Fh, in.LockOwner) + return fuse.Status(err) +} + +func (fs *JFS) Fsync(cancel <-chan struct{}, in *fuse.FsyncIn) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + err := vfs.Fsync(ctx, Ino(in.NodeId), int(in.FsyncFlags), in.Fh) + return fuse.Status(err) +} + +func (fs *JFS) Fallocate(cancel <-chan struct{}, in *fuse.FallocateIn) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + err := vfs.Fallocate(ctx, Ino(in.NodeId), uint8(in.Mode), int64(in.Offset), int64(in.Length), in.Fh) + return fuse.Status(err) +} + +func (fs *JFS) GetLk(cancel <-chan struct{}, in *fuse.LkIn, out *fuse.LkOut) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + l := in.Lk + err := vfs.Getlk(ctx, Ino(in.NodeId), in.Fh, in.Owner, &l.Start, &l.End, &l.Typ, &l.Pid) + if err == 0 { + out.Lk = l + } + return fuse.Status(err) +} + +func (fs *JFS) SetLk(cancel <-chan struct{}, in *fuse.LkIn) (code fuse.Status) { + return fs.setLk(cancel, in, false) +} + +func (fs *JFS) SetLkw(cancel <-chan struct{}, in *fuse.LkIn) (code fuse.Status) { + return fs.setLk(cancel, in, true) +} + +func (fs *JFS) setLk(cancel <-chan struct{}, in *fuse.LkIn, block bool) (code fuse.Status) { + if in.LkFlags&fuse.FUSE_LK_FLOCK != 0 { + return fs.Flock(cancel, in, block) + } + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + l := in.Lk + err := vfs.Setlk(ctx, Ino(in.NodeId), in.Fh, in.Owner, l.Start, l.End, l.Typ, l.Pid, block) + return fuse.Status(err) +} + +func (fs *JFS) Flock(cancel <-chan struct{}, in *fuse.LkIn, block bool) (code fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + err := vfs.Flock(ctx, Ino(in.NodeId), in.Fh, in.Owner, in.Lk.Typ, block) + return fuse.Status(err) +} + +func (fs *JFS) OpenDir(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut) (status fuse.Status) { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + fh, err := vfs.Opendir(ctx, Ino(in.NodeId)) + out.Fh = fh + return fuse.Status(err) +} + +func (fs *JFS) ReadDir(cancel <-chan struct{}, in *fuse.ReadIn, out *fuse.DirEntryList) fuse.Status { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + entries, err := vfs.Readdir(ctx, Ino(in.NodeId), in.Size, int(in.Offset), in.Fh, false) + var de fuse.DirEntry + for _, e := range entries { + de.Ino = uint64(e.Inode) + de.Name = string(e.Name) + de.Mode = e.Attr.SMode() + if !out.AddDirEntry(de) { + break + } + } + return fuse.Status(err) +} + +func (fs *JFS) ReadDirPlus(cancel <-chan struct{}, in *fuse.ReadIn, out *fuse.DirEntryList) fuse.Status { + ctx := newContext(cancel, &in.InHeader) + defer releaseContext(ctx) + entries, err := vfs.Readdir(ctx, Ino(in.NodeId), in.Size, int(in.Offset), in.Fh, true) + var de fuse.DirEntry + for _, e := range entries { + de.Ino = uint64(e.Inode) + de.Name = string(e.Name) + de.Mode = e.Attr.SMode() + eo := out.AddDirLookupEntry(de) + if eo == nil { + break + } + if e.Attr.Full { + vfs.UpdateEntry(e) + fs.replyEntry(eo, e) + } else { + eo.Ino = uint64(e.Inode) + eo.Generation = 1 + } + } + return fuse.Status(err) +} + +var cancelReleaseDir = make(chan struct{}) + +func (fs *JFS) ReleaseDir(in *fuse.ReleaseIn) { + ctx := newContext(cancelReleaseDir, &in.InHeader) + defer releaseContext(ctx) + vfs.Releasedir(ctx, Ino(in.NodeId), in.Fh) +} + +func (fs *JFS) StatFs(cancel <-chan struct{}, in *fuse.InHeader, out *fuse.StatfsOut) (code fuse.Status) { + ctx := newContext(cancel, in) + defer releaseContext(ctx) + st, err := vfs.StatFS(ctx, Ino(in.NodeId)) + if err != 0 { + return fuse.Status(err) + } + out.NameLen = 255 + out.Bsize = st.Bsize + out.Blocks = st.Blocks + out.Bavail = st.Bavail + out.Bfree = st.Bavail + out.Files = st.Files + out.Ffree = st.Favail + out.Frsize = st.Bsize + return 0 +} + +func Main(conf *vfs.Config, options string, attrcacheto_, entrycacheto_, direntrycacheto_ float64) error { + syscall.Setpriority(syscall.PRIO_PROCESS, os.Getpid(), -19) + + imp := NewJFS() + imp.attrTimeout = time.Millisecond * time.Duration(attrcacheto_*1000) + imp.entryTimeout = time.Millisecond * time.Duration(entrycacheto_*1000) + imp.direntryTimeout = time.Millisecond * time.Duration(direntrycacheto_*1000) + + var opt fuse.MountOptions + opt.FsName = "JuiceFS:" + conf.Format.Name + opt.Name = "juicefs" + opt.SingleThreaded = false + opt.MaxBackground = 50 + opt.EnableLocks = true + opt.DisableXAttrs = false + opt.IgnoreSecurityLabels = true + opt.MaxWrite = 1 << 20 + opt.MaxReadAhead = 1 << 20 + opt.DirectMount = true + opt.AllowOther = os.Getuid() == 0 + for _, n := range strings.Split(options, ",") { + if n == "allow_other" || n == "allow_root" { + opt.AllowOther = true + } else if strings.HasPrefix(n, "fsname=") { + opt.FsName = n[len("fsname="):] + if runtime.GOOS == "darwin" { + opt.Options = append(opt.Options, "volname="+n[len("fsname="):]) + } + } else if n == "nonempty" { + } else if n == "debug" { + opt.Debug = true + } else if strings.TrimSpace(n) != "" { + opt.Options = append(opt.Options, n) + } + } + opt.Options = append(opt.Options, "default_permissions") + if runtime.GOOS == "darwin" { + opt.Options = append(opt.Options, "fssubtype=juicefs") + opt.Options = append(opt.Options, "daemon_timeout=60", "iosize=65536", "novncache") + imp.cacheMode = 2 + } + fssrv, err := fuse.NewServer(imp, conf.Mountpoint, &opt) + if err != nil { + return fmt.Errorf("fuse: %s", err) + } + + fssrv.Serve() + return nil +} diff --git a/pkg/fuse/fuse_darwin.go b/pkg/fuse/fuse_darwin.go new file mode 100644 index 000000000000..4b6346ef22db --- /dev/null +++ b/pkg/fuse/fuse_darwin.go @@ -0,0 +1,27 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package fuse + +import ( + "github.com/hanwen/go-fuse/v2/fuse" +) + +func getUmask(in *fuse.MknodIn) uint16 { + return 0 +} + +func setBlksize(out *fuse.Attr, size uint32) { +} diff --git a/pkg/fuse/fuse_linux.go b/pkg/fuse/fuse_linux.go new file mode 100644 index 000000000000..cbcca687c839 --- /dev/null +++ b/pkg/fuse/fuse_linux.go @@ -0,0 +1,28 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package fuse + +import ( + "github.com/hanwen/go-fuse/v2/fuse" +) + +func getUmask(in *fuse.MknodIn) uint16 { + return uint16(in.Umask) +} + +func setBlksize(out *fuse.Attr, size uint32) { + out.Blksize = size +} diff --git a/pkg/fuse/utils.go b/pkg/fuse/utils.go new file mode 100644 index 000000000000..e8befdc805bf --- /dev/null +++ b/pkg/fuse/utils.go @@ -0,0 +1,54 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package fuse + +import ( + "github.com/juicedata/juicefs/pkg/meta" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +func attrToStat(inode Ino, attr *Attr, out *fuse.Attr) { + out.Ino = uint64(inode) + out.Uid = attr.Uid + out.Gid = attr.Gid + out.Mode = attr.SMode() + out.Nlink = attr.Nlink + out.Atime = uint64(attr.Atime) + out.Atimensec = attr.Atimensec + out.Mtime = uint64(attr.Mtime) + out.Mtimensec = attr.Mtimensec + out.Ctime = uint64(attr.Ctime) + out.Ctimensec = attr.Ctimensec + + var size, blocks uint64 + switch attr.Typ { + case meta.TypeDirectory: + fallthrough + case meta.TypeSymlink: + fallthrough + case meta.TypeFile: + size = attr.Length + blocks = (size + 511) / 512 + case meta.TypeBlockDev: + fallthrough + case meta.TypeCharDev: + out.Rdev = attr.Rdev + } + out.Size = size + out.Blocks = blocks + setBlksize(out, 0x10000) +} diff --git a/pkg/meta/config.go b/pkg/meta/config.go new file mode 100644 index 000000000000..464b7a0f7601 --- /dev/null +++ b/pkg/meta/config.go @@ -0,0 +1,34 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package meta + +type Config struct { + Addr string + Password string + IORetries int +} + +type Format struct { + Name string + UUID string + Storage string + Bucket string + AccessKey string + SecretKey string + BlockSize int + Compression string + Partitions int +} diff --git a/pkg/meta/context.go b/pkg/meta/context.go new file mode 100644 index 000000000000..e08502e30bb2 --- /dev/null +++ b/pkg/meta/context.go @@ -0,0 +1,36 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package meta + +type Ino uint64 + +type Context interface { + Gid() uint32 + Uid() uint32 + Pid() uint32 + Cancel() + Canceled() bool +} + +type emptyContext struct{} + +func (ctx emptyContext) Gid() uint32 { return 0 } +func (ctx emptyContext) Uid() uint32 { return 0 } +func (ctx emptyContext) Pid() uint32 { return 1 } +func (ctx emptyContext) Cancel() {} +func (ctx emptyContext) Canceled() bool { return false } + +var Background Context = emptyContext{} diff --git a/pkg/meta/interface.go b/pkg/meta/interface.go new file mode 100644 index 000000000000..ab983fc63801 --- /dev/null +++ b/pkg/meta/interface.go @@ -0,0 +1,143 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package meta + +import ( + "syscall" +) + +const ( + ChunkSize = 1 << 26 // 64M + DeleteChunk = 1000 +) + +const ( + TypeFile = 1 + TypeDirectory = 2 + TypeSymlink = 3 + TypeFIFO = 4 + TypeBlockDev = 5 + TypeCharDev = 6 + TypeSocket = 7 +) + +const ( + SetAttrMode = 1 << iota + SetAttrUID + SetAttrGID + SetAttrSize + SetAttrAtime + SetAttrMtime + SetAttrCtime + SetAttrAtimeNow + SetAttrMtimeNow +) + +type MsgCallback func(...interface{}) error + +type Attr struct { + Flags uint8 + Typ uint8 + Mode uint16 + Uid uint32 + Gid uint32 + Atime int64 + Mtime int64 + Ctime int64 + Atimensec uint32 + Mtimensec uint32 + Ctimensec uint32 + Nlink uint32 + Length uint64 + Rdev uint32 + Full bool +} + +func typeToStatType(_type uint8) uint32 { + switch _type & 0x7F { + case TypeDirectory: + return syscall.S_IFDIR + case TypeSymlink: + return syscall.S_IFLNK + case TypeFile: + return syscall.S_IFREG + case TypeFIFO: + return syscall.S_IFIFO + case TypeSocket: + return syscall.S_IFSOCK + case TypeBlockDev: + return syscall.S_IFBLK + case TypeCharDev: + return syscall.S_IFCHR + default: + panic(_type) + } +} + +func (a Attr) SMode() uint32 { + return typeToStatType(a.Typ) | uint32(a.Mode) +} + +type Entry struct { + Inode Ino + Name []byte + Attr *Attr +} + +type Slice struct { + Chunkid uint64 + Size uint32 + Off uint32 + Len uint32 +} + +type Meta interface { + Init(format Format) error + Load() (*Format, error) + + StatFS(ctx Context, totalspace, availspace, iused, iavail *uint64) syscall.Errno + Access(ctx Context, inode Ino, modemask uint16) syscall.Errno + Lookup(ctx Context, parent Ino, name string, inode *Ino, attr *Attr) syscall.Errno + GetAttr(ctx Context, inode Ino, attr *Attr) syscall.Errno + SetAttr(ctx Context, inode Ino, set uint16, sggidclearmode uint8, attr *Attr) syscall.Errno + Truncate(ctx Context, inode Ino, flags uint8, attrlength uint64, attr *Attr) syscall.Errno + Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size uint64) syscall.Errno + ReadLink(ctx Context, inode Ino, path *[]byte) syscall.Errno + Symlink(ctx Context, parent Ino, name string, path string, inode *Ino, attr *Attr) syscall.Errno + Mknod(ctx Context, parent Ino, name string, _type uint8, mode uint16, cumask uint16, rdev uint32, inode *Ino, attr *Attr) syscall.Errno + Mkdir(ctx Context, parent Ino, name string, mode uint16, cumask uint16, copysgid uint8, inode *Ino, attr *Attr) syscall.Errno + Unlink(ctx Context, parent Ino, name string) syscall.Errno + Rmdir(ctx Context, parent Ino, name string) syscall.Errno + Rename(ctx Context, parentSrc Ino, nameSrc string, parentDst Ino, nameDst string, inode *Ino, attr *Attr) syscall.Errno + Link(ctx Context, inodeSrc, parent Ino, name string, attr *Attr) syscall.Errno + Readdir(ctx Context, inode Ino, wantattr uint8, entries *[]*Entry) syscall.Errno + Create(ctx Context, parent Ino, name string, mode uint16, cumask uint16, inode *Ino, attr *Attr) syscall.Errno + Open(ctx Context, inode Ino, flags uint8, attr *Attr) syscall.Errno + Close(ctx Context, inode Ino) syscall.Errno + Read(inode Ino, indx uint32, chunks *[]Slice) syscall.Errno + NewChunk(ctx Context, inode Ino, indx uint32, offset uint32, chunkid *uint64) syscall.Errno + Write(ctx Context, inode Ino, indx uint32, off uint32, slice Slice) syscall.Errno + + GetXattr(ctx Context, inode Ino, name string, vbuff *[]byte) syscall.Errno + ListXattr(ctx Context, inode Ino, dbuff *[]byte) syscall.Errno + SetXattr(ctx Context, inode Ino, name string, value []byte) syscall.Errno + RemoveXattr(ctx Context, inode Ino, name string) syscall.Errno + Flock(ctx Context, inode Ino, owner uint64, ltype uint32, block bool) syscall.Errno + Getlk(ctx Context, inode Ino, owner uint64, ltype *uint32, start, end *uint64, pid *uint32) syscall.Errno + Setlk(ctx Context, inode Ino, owner uint64, block bool, ltype uint32, start, end uint64, pid uint32) syscall.Errno + + OnMsg(mtype uint32, cb MsgCallback) +} diff --git a/pkg/object/redis.go b/pkg/object/redis.go new file mode 100644 index 000000000000..37c61c46e084 --- /dev/null +++ b/pkg/object/redis.go @@ -0,0 +1,91 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package object + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "strings" + + "github.com/go-redis/redis/v8" + "github.com/juicedata/juicesync/object" +) + +// redisStore stores data chunks into Redis. +type redisStore struct { + object.DefaultObjectStorage + rdb *redis.Client +} + +var c = context.TODO() + +func (r *redisStore) String() string { + return fmt.Sprintf("redis://%s", r.rdb.Options().Addr) +} + +func (r *redisStore) Head(key string) (*object.Object, error) { + v, err := r.rdb.Get(c, key).Bytes() + if err != nil { + return nil, err + } + return &object.Object{Key: key, Size: int64(len(v)), IsDir: strings.HasSuffix(key, "/")}, nil +} + +func (r *redisStore) Get(key string, off, limit int64) (io.ReadCloser, error) { + data, err := r.rdb.Get(c, key).Bytes() + if err != nil { + return nil, err + } + data = data[off:] + if limit > 0 && limit < int64(len(data)) { + data = data[:limit] + } + return ioutil.NopCloser(bytes.NewBuffer(data)), nil +} + +func (r *redisStore) Put(key string, in io.Reader) error { + data, err := ioutil.ReadAll(in) + if err != nil { + return err + } + return r.rdb.Set(c, key, data, 0).Err() +} + +func (r *redisStore) Delete(key string) error { + return r.rdb.Del(c, key).Err() +} + +func newRedis(url, user, passwd string) (object.ObjectStorage, error) { + opt, err := redis.ParseURL(url) + if err != nil { + return nil, fmt.Errorf("parse %s: %s", url, err) + } + if user != "" { + opt.Username = user + } + if passwd != "" { + opt.Password = passwd + } + rdb := redis.NewClient(opt) + return &redisStore{object.DefaultObjectStorage{}, rdb}, nil +} + +func init() { + object.Register("redis", newRedis) +} diff --git a/pkg/object/redis_test.go b/pkg/object/redis_test.go new file mode 100644 index 000000000000..4e09cd5c4b11 --- /dev/null +++ b/pkg/object/redis_test.go @@ -0,0 +1,40 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package object + +import ( + "bytes" + "io/ioutil" + "testing" +) + +func TestRedisStore(t *testing.T) { + s, err := newRedis("redis://127.0.0.1:6379/10", "", "") + if err != nil { + t.Fatalf("create: %s", err) + } + if err := s.Put("chunks/1", bytes.NewBuffer([]byte("data"))); err != nil { + t.Fatalf("put: %s", err) + } + if rb, err := s.Get("chunks/1", 0, -1); err != nil { + t.Fatalf("get : %s", err) + } else if d, err := ioutil.ReadAll(rb); err != nil || !bytes.Equal(d, []byte("data")) { + t.Fatalf("get: %s %s", err, string(d)) + } + if err := s.Delete("chunks/1"); err != nil { + t.Fatalf("delete: %s", err) + } +} diff --git a/pkg/redis/redis.go b/pkg/redis/redis.go new file mode 100644 index 000000000000..7cd49d263b74 --- /dev/null +++ b/pkg/redis/redis.go @@ -0,0 +1,1585 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package redis + +import ( + "context" + "encoding/binary" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "sync" + "syscall" + "time" + + . "github.com/juicedata/juicefs/pkg/meta" + "github.com/juicedata/juicefs/pkg/utils" + + "github.com/go-redis/redis/v8" +) + +/* + Node: i$inode -> Attribute{type,mode,uid,gid,atime,mtime,ctime,nlink,length,rdev} + Dir: d$inode -> {name -> {inode,type}} + File: c$inode_$indx -> [Slice{pos,id,length,off,len}] + Symlink: s$inode -> target + Xattr: x$inode -> {name -> value} + Flock: lockf$inode -> { $sid_$owner -> ltype } + POSIX lock: lockp$inode -> { $sid_$owner -> Plock(pid,ltype,start,end) } + Sessions: sessions -> [ $sid -> heartbeat ] + Removed chunks: delchunks -> [($inode,$start,$end,$maxchunk) -> seconds] +*/ + +var logger = utils.GetLogger("juicefs") + +const usedSpace = "usedSpace" +const totalInodes = "totalInodes" +const delchunks = "delchunks" +const allSessions = "sessions" + +type RedisConfig struct { + Strict bool // update ctime + Retries int +} + +type redisMeta struct { + sync.Mutex + conf *RedisConfig + rdb *redis.Client + + sid int64 + openFiles map[Ino]int + removedFiles map[Ino]bool + msgCallbacks *msgCallbacks +} + +type msgCallbacks struct { + sync.Mutex + callbacks map[uint32]MsgCallback +} + +func NewRedisMeta(url string, conf *RedisConfig) (Meta, error) { + opt, err := redis.ParseURL(url) + if err != nil { + return nil, fmt.Errorf("parse %s: %s", url, err) + } + if opt.Password == "" && os.Getenv("REDIS_PASSWD") != "" { + opt.Password = os.Getenv("REDIS_PASSWD") + } + m := &redisMeta{ + conf: conf, + rdb: redis.NewClient(opt), + openFiles: make(map[Ino]int), + removedFiles: make(map[Ino]bool), + msgCallbacks: &msgCallbacks{ + callbacks: make(map[uint32]MsgCallback), + }, + } + m.sid, err = m.rdb.Incr(c, "nextsession").Result() + if err != nil { + return nil, fmt.Errorf("create session: %s", err) + } + logger.Debugf("session is is %d", m.sid) + go m.refreshSession() + go m.cleanupChunks() + return m, nil +} + +func (r *redisMeta) Init(format Format) error { + body, err := r.rdb.Get(c, "setting").Result() + if err != nil && err != redis.Nil { + return err + } + if err == nil { + return fmt.Errorf("this volume is already formated as: %s", body) + } + data, err := json.MarshalIndent(format, "", "") + if err != nil { + logger.Fatalf("json: %s", err) + } + return r.rdb.Set(c, "setting", data, 0).Err() +} + +func (r *redisMeta) Load() (*Format, error) { + body, err := r.rdb.Get(c, "setting").Result() + if err == redis.Nil { + return nil, fmt.Errorf("no volume found") + } + if err != nil { + return nil, err + } + var format Format + err = json.Unmarshal([]byte(body), &format) + if err != nil { + return nil, fmt.Errorf("json: %s", err) + } + return &format, nil +} + +func (r *redisMeta) OnMsg(mtype uint32, cb MsgCallback) { + r.msgCallbacks.Lock() + defer r.msgCallbacks.Unlock() + r.msgCallbacks.callbacks[mtype] = cb +} + +func (r *redisMeta) newMsg(mid uint32, args ...interface{}) error { + r.msgCallbacks.Lock() + cb, ok := r.msgCallbacks.callbacks[mid] + r.msgCallbacks.Unlock() + if ok { + return cb(args...) + } + panic("not callback for " + strconv.Itoa(int(mid))) +} + +var c = context.TODO() + +func (r *redisMeta) sessionKey(sid int64) string { + return fmt.Sprintf("session%d", r.sid) +} + +func (r *redisMeta) symKey(inode Ino) string { + return fmt.Sprintf("s%d", inode) +} + +func (r *redisMeta) inodeKey(inode Ino) string { + return fmt.Sprintf("i%d", inode) +} + +func (r *redisMeta) entryKey(parent Ino) string { + return fmt.Sprintf("d%d", parent) +} + +func (r *redisMeta) chunkKey(inode Ino, indx uint32) string { + return fmt.Sprintf("c%d_%d", inode, indx) +} + +func (r *redisMeta) xattrKey(inode Ino) string { + return fmt.Sprintf("x%d", inode) +} + +func (r *redisMeta) flockKey(inode Ino) string { + return fmt.Sprintf("lockf%d", inode) +} + +func (r *redisMeta) ownerKey(owner uint64) string { + return fmt.Sprintf("%d_%016X", r.sid, owner) +} + +func (r *redisMeta) plockKey(inode Ino) string { + return fmt.Sprintf("lockp%d", inode) +} + +func (r *redisMeta) nextInode() (Ino, error) { + ino, err := r.rdb.Incr(c, "nextinode").Uint64() + if ino == 1 { + ino, err = r.rdb.Incr(c, "nextinode").Uint64() + } + return Ino(ino), err +} + +func (r *redisMeta) packEntry(_type uint8, inode Ino) []byte { + wb := utils.NewBuffer(9) + wb.Put8(_type) + wb.Put64(uint64(inode)) + return wb.Bytes() +} + +func (r *redisMeta) parseEntry(buf []byte) (uint8, Ino) { + if len(buf) != 9 { + panic("invalid entry") + } + return buf[0], Ino(binary.BigEndian.Uint64(buf[1:])) +} + +func (r *redisMeta) parseAttr(buf []byte, attr *Attr) { + if attr == nil { + return + } + rb := utils.FromBuffer(buf) + attr.Flags = rb.Get8() + attr.Mode = rb.Get16() + attr.Typ = uint8(attr.Mode >> 12) + attr.Mode &= 0xfff + attr.Uid = rb.Get32() + attr.Gid = rb.Get32() + attr.Atime = int64(rb.Get64()) + attr.Atimensec = rb.Get32() + attr.Mtime = int64(rb.Get64()) + attr.Mtimensec = rb.Get32() + attr.Ctime = int64(rb.Get64()) + attr.Ctimensec = rb.Get32() + attr.Nlink = rb.Get32() + attr.Length = rb.Get64() + attr.Rdev = rb.Get32() + attr.Full = true + logger.Tracef("attr: %+v -> %+v", buf, attr) +} + +func (r *redisMeta) marshal(attr *Attr) []byte { + w := utils.NewBuffer(36 + 24 + 4) + w.Put8(attr.Flags) + w.Put16((uint16(attr.Typ) << 12) | (attr.Mode & 0xfff)) + w.Put32(attr.Uid) + w.Put32(attr.Gid) + w.Put64(uint64(attr.Atime)) + w.Put32(attr.Atimensec) + w.Put64(uint64(attr.Mtime)) + w.Put32(attr.Mtimensec) + w.Put64(uint64(attr.Ctime)) + w.Put32(attr.Ctimensec) + w.Put32(attr.Nlink) + w.Put64(attr.Length) + w.Put32(attr.Rdev) + logger.Tracef("attr: %+v -> %+v", attr, w.Bytes()) + return w.Bytes() +} + +func align4K(length uint64) int64 { + if length == 0 { + return 0 + } + return int64((((length - 1) >> 12) + 1) << 12) +} + +func (r *redisMeta) StatFS(ctx Context, totalspace, availspace, iused, iavail *uint64) syscall.Errno { + *totalspace = 1 << 50 + used, _ := r.rdb.IncrBy(c, usedSpace, 0).Result() + used = ((used >> 16) + 1) << 16 // aligned to 64K + *availspace = *totalspace - uint64(used) + inodes, _ := r.rdb.IncrBy(c, totalInodes, 0).Result() + *iused = uint64(inodes) + *iavail = 10 << 20 + return 0 +} + +func (r *redisMeta) Lookup(ctx Context, parent Ino, name string, inode *Ino, attr *Attr) syscall.Errno { + buf, err := r.rdb.HGet(c, r.entryKey(parent), name).Bytes() + if err != nil { + return errno(err) + } + _, ino := r.parseEntry(buf) + a, err := r.rdb.Get(c, r.inodeKey(ino)).Bytes() + if err == nil && attr != nil { + r.parseAttr(a, attr) + if attr.Typ == TypeDirectory && r.conf.Strict { + cnt, err := r.rdb.HLen(c, r.entryKey(ino)).Result() + if err == nil { + attr.Nlink = uint32(cnt + 2) + } + } + } + if inode != nil { + *inode = ino + } + return errno(err) +} + +func (r *redisMeta) Access(ctx Context, inode Ino, modemask uint16) syscall.Errno { + return 0 // handled by kernel +} + +func (r *redisMeta) GetAttr(ctx Context, inode Ino, attr *Attr) syscall.Errno { + a, err := r.rdb.Get(c, r.inodeKey(inode)).Bytes() + if inode == 1 && err == redis.Nil { + // root inode + attr.Flags = 0 + attr.Typ = TypeDirectory + attr.Mode = 0777 + attr.Uid = 0 + attr.Uid = 0 + ts := time.Now().Unix() + attr.Atime = ts + attr.Mtime = ts + attr.Ctime = ts + attr.Nlink = 2 + attr.Length = 4 << 10 + attr.Rdev = 0 + r.rdb.Set(c, r.inodeKey(inode), r.marshal(attr), 0) + return 0 + } + if err == nil { + r.parseAttr(a, attr) + if attr.Typ == TypeDirectory && r.conf.Strict { + cnt, err := r.rdb.HLen(c, r.entryKey(inode)).Result() + if err == nil { + attr.Nlink = uint32(cnt + 2) + } + } + } + return errno(err) +} + +func errno(err error) syscall.Errno { + if err == nil { + return 0 + } + if eno, ok := err.(syscall.Errno); ok { + return eno + } + if err == redis.Nil { + return syscall.ENOENT + } + logger.Errorf("error: %s", err) + return syscall.EIO +} + +func (r *redisMeta) txn(txf func(tx *redis.Tx) error, keys ...string) syscall.Errno { + var err error + for i := 0; i < 10; i++ { + err = r.rdb.Watch(c, txf, keys...) + if err == nil { + return 0 + } + if err == redis.TxFailedErr { + continue + } + return errno(err) + } + return errno(err) +} + +func (r *redisMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64, attr *Attr) syscall.Errno { + maxchunk, err := r.rdb.IncrBy(c, "nextchunk", 0).Uint64() + if err != nil { + return errno(err) + } + return r.txn(func(tx *redis.Tx) error { + var t Attr + a, err := tx.Get(c, r.inodeKey(inode)).Bytes() + if err != nil { + return err + } + r.parseAttr(a, &t) + if t.Typ != TypeFile { + return syscall.EPERM + } + old := t.Length + t.Length = length + now := time.Now() + t.Mtime = now.Unix() + t.Mtimensec = uint32(now.Nanosecond()) + t.Ctime = now.Unix() + t.Ctimensec = uint32(now.Nanosecond()) + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.Set(c, r.inodeKey(inode), r.marshal(&t), 0) + if old > length { + pipe.ZAdd(c, delchunks, &redis.Z{float64(now.Unix()), r.delChunks(inode, length, old, maxchunk)}) + } else if length > (old/ChunkSize+1)*ChunkSize { + // zero out last chunks + w := utils.NewBuffer(24) + w.Put32(uint32(old % ChunkSize)) + w.Put64(0) + w.Put32(0) + w.Put32(0) + w.Put32(ChunkSize - uint32(old%ChunkSize)) + pipe.RPush(c, r.chunkKey(inode, uint32(old/ChunkSize)), w.Bytes()) + } + pipe.IncrBy(c, usedSpace, align4K(length)-align4K(old)) + return nil + }) + if err == nil { + if attr != nil { + *attr = t + } + go r.deleteChunks(inode, length, old, maxchunk) + } + return err + }, r.inodeKey(inode)) +} + +const ( + // fallocate + fallocKeepSize = 0x01 + fallocPunchHole = 0x02 + fallocNoHideStale = 0x04 // reserved + fallocCollapesRange = 0x08 + fallocZeroRange = 0x10 + fallocInsertRange = 0x20 +) + +func (r *redisMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size uint64) syscall.Errno { + if mode&fallocCollapesRange != 0 && mode != fallocCollapesRange { + return syscall.EINVAL + } + if mode&fallocInsertRange != 0 && mode != fallocInsertRange { + return syscall.EINVAL + } + if mode == fallocInsertRange || mode == fallocCollapesRange { + return syscall.ENOTSUP + } + if mode&fallocPunchHole != 0 && mode&fallocKeepSize == 0 { + return syscall.EINVAL + } + if size == 0 { + return syscall.EINVAL + } + return r.txn(func(tx *redis.Tx) error { + var t Attr + a, err := tx.Get(c, r.inodeKey(inode)).Bytes() + if err != nil { + return err + } + r.parseAttr(a, &t) + if t.Typ == TypeFIFO { + return syscall.EPIPE + } + if t.Typ != TypeFile { + return syscall.EPERM + } + length := t.Length + if off+size > t.Length { + if mode&fallocKeepSize == 0 { + length = off + size + } + } + + old := t.Length + t.Length = length + now := time.Now() + t.Ctime = now.Unix() + t.Ctimensec = uint32(now.Nanosecond()) + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.Set(c, r.inodeKey(inode), r.marshal(&t), 0) + if mode&(fallocZeroRange|fallocPunchHole) != 0 { + if off+size > old { + size = old - off + } + for size > 0 { + indx := uint32(off / ChunkSize) + coff := off % ChunkSize + l := size + if coff+size > ChunkSize { + l = ChunkSize - coff + } + w := utils.NewBuffer(24) + w.Put32(uint32(coff)) + w.Put64(0) + w.Put32(0) + w.Put32(0) + w.Put32(uint32(l)) + pipe.RPush(c, r.chunkKey(inode, indx), w.Bytes()) + off += l + size -= l + } + } + pipe.IncrBy(c, usedSpace, align4K(length)-align4K(old)) + return nil + }) + return err + }, r.inodeKey(inode)) +} + +func (r *redisMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint8, attr *Attr) syscall.Errno { + return r.txn(func(tx *redis.Tx) error { + var cur Attr + a, err := tx.Get(c, r.inodeKey(inode)).Bytes() + if err != nil { + return err + } + r.parseAttr(a, &cur) + if (set&(SetAttrUID|SetAttrGID)) != 0 && (set&SetAttrMode) != 0 { + attr.Mode |= (cur.Mode & 06000) + } + if (cur.Mode&06000) != 0 && (set&(SetAttrUID|SetAttrGID)) != 0 { + cur.Mode &= 01777 + attr.Mode &= 01777 + } + if set&SetAttrUID != 0 { + cur.Uid = attr.Uid + } + if set&SetAttrGID != 0 { + cur.Gid = attr.Gid + } + if set&SetAttrMode != 0 { + if ctx.Uid() != 0 && (attr.Mode&02000) != 0 { + if ctx.Gid() != cur.Gid { + attr.Mode &= 05777 + } + } + cur.Mode = attr.Mode + } + now := time.Now() + if set&SetAttrAtime != 0 { + cur.Atime = attr.Atime + cur.Atimensec = attr.Atimensec + } + if set&SetAttrAtimeNow != 0 { + cur.Atime = now.Unix() + cur.Atimensec = uint32(now.Nanosecond()) + } + if set&SetAttrMtime != 0 { + cur.Mtime = attr.Mtime + cur.Mtimensec = attr.Mtimensec + } + if set&SetAttrMtimeNow != 0 { + cur.Mtime = now.Unix() + cur.Mtimensec = uint32(now.Nanosecond()) + } + cur.Ctime = now.Unix() + cur.Ctimensec = uint32(now.Nanosecond()) + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.Set(c, r.inodeKey(inode), r.marshal(&cur), 0) + return nil + }) + if err == nil { + *attr = cur + } + return err + }, r.inodeKey(inode)) +} + +func (r *redisMeta) ReadLink(ctx Context, inode Ino, path *[]byte) syscall.Errno { + buf, err := r.rdb.Get(c, r.symKey(inode)).Bytes() + if err == nil { + *path = buf + } + return errno(err) +} + +func (r *redisMeta) Symlink(ctx Context, parent Ino, name string, path string, inode *Ino, attr *Attr) syscall.Errno { + return r.mknod(ctx, parent, name, TypeSymlink, 0644, 022, 0, path, inode, attr) +} + +func (r *redisMeta) Mknod(ctx Context, parent Ino, name string, _type uint8, mode, cumask uint16, rdev uint32, inode *Ino, attr *Attr) syscall.Errno { + return r.mknod(ctx, parent, name, _type, mode, cumask, rdev, "", inode, attr) +} + +func (r *redisMeta) mknod(ctx Context, parent Ino, name string, _type uint8, mode, cumask uint16, rdev uint32, path string, inode *Ino, attr *Attr) syscall.Errno { + ino, err := r.nextInode() + if err != nil { + return errno(err) + } + attr.Typ = _type + attr.Mode = mode & ^cumask + attr.Uid = ctx.Uid() + attr.Gid = ctx.Gid() + if _type == TypeDirectory { + attr.Nlink = 2 + attr.Length = 4 << 10 + } else { + attr.Nlink = 1 + if _type == TypeSymlink { + attr.Length = uint64(len(path)) + } else { + attr.Length = 0 + attr.Rdev = rdev + } + } + + *inode = ino + return r.txn(func(tx *redis.Tx) error { + var patt Attr + a, err := tx.Get(c, r.inodeKey(parent)).Bytes() + if err != nil { + return err + } + r.parseAttr(a, &patt) + if patt.Typ != TypeDirectory { + return syscall.ENOTDIR + } + + err = tx.HGet(c, r.entryKey(parent), name).Err() + if err != nil && err != redis.Nil { + return err + } else if err == nil { + return syscall.EEXIST + } + + now := time.Now() + patt.Mtime = now.Unix() + patt.Mtimensec = uint32(now.Nanosecond()) + patt.Ctime = now.Unix() + patt.Ctimensec = uint32(now.Nanosecond()) + attr.Atime = now.Unix() + attr.Atimensec = uint32(now.Nanosecond()) + attr.Mtime = now.Unix() + attr.Mtimensec = uint32(now.Nanosecond()) + attr.Ctime = now.Unix() + attr.Ctimensec = uint32(now.Nanosecond()) + + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.HSet(c, r.entryKey(parent), name, r.packEntry(_type, ino)) + pipe.Set(c, r.inodeKey(parent), r.marshal(&patt), 0) + pipe.Set(c, r.inodeKey(ino), r.marshal(attr), 0) + if _type == TypeSymlink { + pipe.Set(c, r.symKey(ino), path, 0) + } else if _type == TypeFile { + pipe.IncrBy(c, usedSpace, align4K(0)) + } + pipe.Incr(c, totalInodes) + return nil + }) + return err + }, r.inodeKey(parent), r.entryKey(parent)) +} + +func (r *redisMeta) Mkdir(ctx Context, parent Ino, name string, mode uint16, cumask uint16, copysgid uint8, inode *Ino, attr *Attr) syscall.Errno { + return r.Mknod(ctx, parent, name, TypeDirectory, mode, cumask, 0, inode, attr) +} + +func (r *redisMeta) Create(ctx Context, parent Ino, name string, mode uint16, cumask uint16, inode *Ino, attr *Attr) syscall.Errno { + err := r.Mknod(ctx, parent, name, TypeFile, mode, cumask, 0, inode, attr) + if err == 0 { + r.Lock() + r.openFiles[*inode] = 1 + r.Unlock() + } + return err +} + +func (r *redisMeta) Unlink(ctx Context, parent Ino, name string) syscall.Errno { + buf, err := r.rdb.HGet(c, r.entryKey(parent), name).Bytes() + if err != nil { + return errno(err) + } + _type, inode := r.parseEntry(buf) + if _type == TypeDirectory { + return syscall.EPERM + } + + return r.txn(func(tx *redis.Tx) error { + rs, _ := tx.MGet(c, r.inodeKey(parent), r.inodeKey(inode)).Result() + if rs[0] == nil || rs[1] == nil { + return redis.Nil + } + var pattr, attr Attr + r.parseAttr([]byte(rs[0].(string)), &pattr) + if pattr.Typ != TypeDirectory { + return syscall.ENOTDIR + } + now := time.Now() + pattr.Mtime = now.Unix() + pattr.Mtimensec = uint32(now.Nanosecond()) + pattr.Ctime = now.Unix() + pattr.Ctimensec = uint32(now.Nanosecond()) + r.parseAttr([]byte(rs[1].(string)), &attr) + attr.Ctime = now.Unix() + attr.Ctimensec = uint32(now.Nanosecond()) + + buf, err := tx.HGet(c, r.entryKey(parent), name).Bytes() + if err != nil { + return err + } + _type2, inode2 := r.parseEntry(buf) + if _type2 != _type || inode2 != inode { + return syscall.EAGAIN + } + + attr.Nlink-- + var opened bool + var maxchunk uint64 + if _type == TypeFile && attr.Nlink == 0 { + r.Lock() + opened = r.openFiles[inode] > 0 + r.Unlock() + if !opened { + maxchunk, err = tx.IncrBy(c, "nextchunk", 0).Uint64() + if err != nil { + return err + } + } + } + + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.HDel(c, r.entryKey(parent), name) + pipe.Set(c, r.inodeKey(parent), r.marshal(&pattr), 0) + if attr.Nlink > 0 { + pipe.Set(c, r.inodeKey(inode), r.marshal(&attr), 0) + } else { + switch _type { + case TypeSymlink: + pipe.Del(c, r.symKey(inode)) + pipe.Del(c, r.inodeKey(inode)) + case TypeFile: + if opened { + pipe.Set(c, r.inodeKey(inode), r.marshal(&attr), 0) + pipe.SAdd(c, r.sessionKey(r.sid), strconv.Itoa(int(inode))) + } else { + pipe.ZAdd(c, delchunks, &redis.Z{float64(now.Unix()), r.delChunks(inode, 0, attr.Length, maxchunk)}) + pipe.Del(c, r.inodeKey(inode)) + pipe.IncrBy(c, usedSpace, -align4K(attr.Length)) + } + } + pipe.IncrBy(c, totalInodes, -1) + } + return nil + }) + if err == nil && _type == TypeFile && attr.Nlink == 0 { + if opened { + r.Lock() + r.removedFiles[inode] = true + r.Unlock() + } else { + go r.deleteChunks(inode, 0, attr.Length, maxchunk) + } + } + return err + }, r.entryKey(parent), r.inodeKey(parent), r.inodeKey(inode)) +} + +func (r *redisMeta) Rmdir(ctx Context, parent Ino, name string) syscall.Errno { + if name == "." { + return syscall.EINVAL + } + if name == ".." { + return syscall.ENOTEMPTY + } + buf, err := r.rdb.HGet(c, r.entryKey(parent), name).Bytes() + if err != nil { + return errno(err) + } + typ, inode := r.parseEntry(buf) + if typ != TypeDirectory { + return syscall.ENOTDIR + } + + return r.txn(func(tx *redis.Tx) error { + a, err := tx.Get(c, r.inodeKey(parent)).Bytes() + if err != nil { + return err + } + var pattr Attr + r.parseAttr(a, &pattr) + if pattr.Typ != TypeDirectory { + return syscall.ENOTDIR + } + now := time.Now() + pattr.Mtime = now.Unix() + pattr.Mtimensec = uint32(now.Nanosecond()) + pattr.Ctime = now.Unix() + pattr.Ctimensec = uint32(now.Nanosecond()) + + buf, err := tx.HGet(c, r.entryKey(parent), name).Bytes() + if err != nil { + return err + } + typ, inode = r.parseEntry(buf) + if typ != TypeDirectory { + return syscall.ENOTDIR + } + + cnt, err := tx.HLen(c, r.entryKey(inode)).Result() + if err != nil { + return err + } + if cnt > 0 { + return syscall.ENOTEMPTY + } + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.HDel(c, r.entryKey(parent), name) + pipe.Set(c, r.inodeKey(parent), r.marshal(&pattr), 0) + pipe.Del(c, r.inodeKey(inode)) + // pipe.Del(c, r.entryKey(inode)) + pipe.IncrBy(c, totalInodes, -1) + return nil + }) + return err + }, r.inodeKey(parent), r.entryKey(parent), r.inodeKey(inode), r.entryKey(inode)) +} + +func (r *redisMeta) Rename(ctx Context, parentSrc Ino, nameSrc string, parentDst Ino, nameDst string, inode *Ino, attr *Attr) syscall.Errno { + buf, err := r.rdb.HGet(c, r.entryKey(parentSrc), nameSrc).Bytes() + if err != nil { + return errno(err) + } + _, ino := r.parseEntry(buf) + if parentSrc == parentDst && nameSrc == nameDst { + if inode != nil { + *inode = ino + } + return 0 + } + buf, err = r.rdb.HGet(c, r.entryKey(parentDst), nameDst).Bytes() + if err != nil && err != redis.Nil { + return errno(err) + } + keys := []string{r.entryKey(parentSrc), r.inodeKey(parentSrc), r.inodeKey(ino), r.entryKey(parentDst), r.inodeKey(parentDst)} + + var dino Ino + var dtyp uint8 + if err == nil { + dtyp, dino = r.parseEntry(buf) + keys = append(keys, r.inodeKey(dino)) + if dtyp == TypeDirectory { + keys = append(keys, r.entryKey(dino)) + } + } + + return r.txn(func(tx *redis.Tx) error { + buf, err = tx.HGet(c, r.entryKey(parentDst), nameDst).Bytes() + if err != nil && err != redis.Nil { + return err + } + var tattr Attr + var opened bool + var maxchunk uint64 + if err == nil { + typ1, dino1 := r.parseEntry(buf) + if dino1 != dino || typ1 != dtyp { + return syscall.EAGAIN + } + if typ1 == TypeDirectory { + cnt, err := tx.HLen(c, r.entryKey(dino)).Result() + if err != nil { + return err + } + if cnt != 0 { + return syscall.ENOTEMPTY + } + } else { + a, err := tx.Get(c, r.inodeKey(dino)).Bytes() + if err != nil { + return err + } + r.parseAttr(a, &tattr) + tattr.Nlink-- + if tattr.Nlink > 0 { + now := time.Now() + tattr.Ctime = now.Unix() + tattr.Ctimensec = uint32(now.Nanosecond()) + } else if dtyp == TypeFile { + r.Lock() + opened = r.openFiles[dino] > 0 + r.Unlock() + if !opened { + maxchunk, err = tx.IncrBy(c, "nextchunk", 0).Uint64() + if err != nil { + return err + } + } + } + } + } else { + dino = 0 + } + + buf, err := tx.HGet(c, r.entryKey(parentSrc), nameSrc).Bytes() + if err != nil { + return err + } + _, ino1 := r.parseEntry(buf) + if ino != ino1 { + return syscall.EAGAIN + } + + rs, _ := tx.MGet(c, r.inodeKey(parentSrc), r.inodeKey(parentDst), r.inodeKey(ino)).Result() + if rs[0] == nil || rs[1] == nil || rs[2] == nil { + return redis.Nil + } + var sattr, dattr, iattr Attr + r.parseAttr([]byte(rs[0].(string)), &sattr) + if sattr.Typ != TypeDirectory { + return syscall.ENOTDIR + } + now := time.Now() + sattr.Mtime = now.Unix() + sattr.Mtimensec = uint32(now.Nanosecond()) + sattr.Ctime = now.Unix() + sattr.Ctimensec = uint32(now.Nanosecond()) + r.parseAttr([]byte(rs[1].(string)), &dattr) + if dattr.Typ != TypeDirectory { + return syscall.ENOTDIR + } + dattr.Mtime = now.Unix() + dattr.Mtimensec = uint32(now.Nanosecond()) + dattr.Ctime = now.Unix() + dattr.Ctimensec = uint32(now.Nanosecond()) + r.parseAttr([]byte(rs[2].(string)), &iattr) + iattr.Ctime = now.Unix() + iattr.Ctimensec = uint32(now.Nanosecond()) + if attr != nil { + *attr = iattr + } + + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.HDel(c, r.entryKey(parentSrc), nameSrc) + pipe.Set(c, r.inodeKey(parentSrc), r.marshal(&sattr), 0) + if dino > 0 { + if tattr.Nlink > 0 { + pipe.Set(c, r.inodeKey(dino), r.marshal(&tattr), 0) + } else { + if dtyp == TypeDirectory { + // pipe.Del(c, r.entryKey(dino)) + pipe.Del(c, r.inodeKey(dino)) + } else if dtyp == TypeSymlink { + pipe.Del(c, r.symKey(dino)) + pipe.Del(c, r.inodeKey(dino)) + } else if dtyp == TypeFile { + if opened { + pipe.Set(c, r.inodeKey(dino), r.marshal(&tattr), 0) + pipe.SAdd(c, r.sessionKey(r.sid), strconv.Itoa(int(dino))) + } else { + pipe.ZAdd(c, delchunks, &redis.Z{float64(now.Unix()), r.delChunks(dino, 0, tattr.Length, maxchunk)}) + pipe.Del(c, r.inodeKey(dino)) + pipe.IncrBy(c, usedSpace, -align4K(tattr.Length)) + } + } + pipe.IncrBy(c, totalInodes, -1) + } + pipe.HDel(c, r.entryKey(parentDst), nameDst) + } + pipe.HSet(c, r.entryKey(parentDst), nameDst, buf) + if parentDst != parentSrc { + pipe.Set(c, r.inodeKey(parentDst), r.marshal(&dattr), 0) + } + pipe.Set(c, r.inodeKey(ino), r.marshal(&iattr), 0) + return nil + }) + if err == nil && dino > 0 && dtyp == TypeFile { + if opened { + r.Lock() + r.removedFiles[dino] = true + r.Unlock() + } else { + go r.deleteChunks(dino, 0, tattr.Length, maxchunk) + } + } + return err + }, keys...) +} + +func (r *redisMeta) Link(ctx Context, inode, parent Ino, name string, attr *Attr) syscall.Errno { + return r.txn(func(tx *redis.Tx) error { + rs, _ := tx.MGet(c, r.inodeKey(parent), r.inodeKey(inode)).Result() + if rs[0] == nil || rs[1] == nil { + return redis.Nil + } + var pattr, iattr Attr + r.parseAttr([]byte(rs[0].(string)), &pattr) + if pattr.Typ != TypeDirectory { + return syscall.ENOTDIR + } + now := time.Now() + pattr.Mtime = now.Unix() + pattr.Mtimensec = uint32(now.Nanosecond()) + pattr.Ctime = now.Unix() + pattr.Ctimensec = uint32(now.Nanosecond()) + r.parseAttr([]byte(rs[1].(string)), &iattr) + if iattr.Typ == TypeDirectory { + return syscall.EPERM + } + iattr.Ctime = now.Unix() + iattr.Ctimensec = uint32(now.Nanosecond()) + iattr.Nlink++ + + err := tx.HGet(c, r.entryKey(parent), name).Err() + if err != nil && err != redis.Nil { + return err + } else if err == nil { + return syscall.EEXIST + } + + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.HSet(c, r.entryKey(parent), name, r.packEntry(iattr.Typ, inode)) + pipe.Set(c, r.inodeKey(parent), r.marshal(&pattr), 0) + pipe.Set(c, r.inodeKey(inode), r.marshal(&iattr), 0) + return nil + }) + if err == nil && attr != nil { + *attr = iattr + } + return err + }, r.inodeKey(inode), r.entryKey(parent), r.inodeKey(parent)) +} + +func (r *redisMeta) Readdir(ctx Context, inode Ino, plus uint8, entries *[]*Entry) syscall.Errno { + vals, err := r.rdb.HGetAll(c, r.entryKey(inode)).Result() + if err != nil { + return errno(err) + } + for name, val := range vals { + typ, inode := r.parseEntry([]byte(val)) + *entries = append(*entries, &Entry{ + Inode: inode, + Name: []byte(name), + Attr: &Attr{Typ: typ}, + }) + } + if plus != 0 { + var keys []string + for _, e := range *entries { + keys = append(keys, r.inodeKey(e.Inode)) + } + rs, _ := r.rdb.MGet(c, keys...).Result() + for i, re := range rs { + if re != nil { + if a, ok := re.([]byte); ok { + r.parseAttr(a, (*entries)[i].Attr) + } + } + } + } + return 0 +} + +func (r *redisMeta) cleanStaleSession(sid int64) { + inodes, err := r.rdb.LRange(c, r.sessionKey(sid), 0, 1000).Result() + if err != nil { + return + } + for _, sinode := range inodes { + inode, _ := strconv.Atoi(sinode) + r.deleteInode(Ino(inode)) + } + if len(inodes) == 0 { + r.rdb.Del(c, r.sessionKey(sid)) + r.rdb.ZRem(c, allSessions, strconv.Itoa(int(sid))) + } +} + +func (r *redisMeta) cleanStaleSessions() { + now := time.Now() + rng := &redis.ZRangeBy{"", strconv.Itoa(int(now.Add(time.Minute * -10).Unix())), 0, 100} + staleSessions, _ := r.rdb.ZRangeByScore(c, allSessions, rng).Result() + for _, ssid := range staleSessions { + sid, _ := strconv.Atoi(ssid) + r.cleanStaleSession(int64(sid)) + } + + rng = &redis.ZRangeBy{"", strconv.Itoa(int(now.Add(time.Minute * -3).Unix())), 0, 100} + staleSessions, err := r.rdb.ZRangeByScore(c, allSessions, rng).Result() + if err != nil || len(staleSessions) == 0 { + return + } + sids := make(map[string]bool) + for _, sid := range staleSessions { + sids[sid] = true + } + var cursor uint64 + var keys []string + for { + keys, cursor, err = r.rdb.Scan(c, cursor, "lock*", 1000).Result() + if err != nil || len(keys) == 0 { + break + } + for _, k := range keys { + owners, _ := r.rdb.HKeys(c, k).Result() + for _, o := range owners { + p := strings.Split(o, "_")[0] + if _, ok := sids[p]; ok { + err = r.rdb.HDel(c, k, o).Err() + logger.Infof("cleanup lock on %s from session %s: %s", k, p, err) + } + } + } + } +} + +func (r *redisMeta) refreshSession() { + for { + now := time.Now() + r.rdb.ZAdd(c, allSessions, &redis.Z{float64(now.Unix()), strconv.Itoa(int(r.sid))}) + go r.cleanStaleSessions() + time.Sleep(time.Minute) + } +} + +func (r *redisMeta) deleteInode(inode Ino) error { + var attr Attr + a, err := r.rdb.Get(c, r.inodeKey(inode)).Bytes() + if err == redis.Nil { + return nil + } + if err != nil { + return err + } + maxchunk, err := r.rdb.IncrBy(c, "nextchunk", 0).Uint64() + if err != nil { + return err + } + r.parseAttr(a, &attr) + _, err = r.rdb.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.ZAdd(c, delchunks, &redis.Z{float64(time.Now().Unix()), r.delChunks(inode, 0, attr.Length, maxchunk)}) + pipe.Del(c, r.inodeKey(inode)) + pipe.IncrBy(c, usedSpace, -align4K(attr.Length)) + return nil + }) + if err == nil { + go r.deleteChunks(inode, 0, attr.Length, maxchunk) + } + return err +} + +func (r *redisMeta) Open(ctx Context, inode Ino, flags uint8, attr *Attr) syscall.Errno { + var err syscall.Errno + if attr != nil { + err = r.GetAttr(ctx, inode, attr) + } + if err == 0 { + r.Lock() + r.openFiles[inode] = r.openFiles[inode] + 1 + r.Unlock() + } + return 0 +} + +func (r *redisMeta) Close(ctx Context, inode Ino) syscall.Errno { + r.Lock() + defer r.Unlock() + refs := r.openFiles[inode] + if refs <= 1 { + delete(r.openFiles, inode) + if r.removedFiles[inode] { + delete(r.removedFiles, inode) + go func() { + if err := r.deleteInode(inode); err == nil { + r.rdb.SRem(c, r.sessionKey(r.sid), strconv.Itoa(int(inode))) + } + }() + } + } else { + r.openFiles[inode] = refs - 1 + } + return 0 +} + +func (r *redisMeta) Read(inode Ino, indx uint32, chunks *[]Slice) syscall.Errno { + vals, err := r.rdb.LRange(c, r.chunkKey(inode, indx), 0, 1000000).Result() + if err != nil { + return errno(err) + } + var root *slice + for _, val := range vals { + rb := utils.ReadBuffer([]byte(val)) + pos := rb.Get32() + chunkid := rb.Get64() + cleng := rb.Get32() + soff := rb.Get32() + slen := rb.Get32() + s := newSlice(pos, chunkid, cleng, soff, slen) + if root != nil { + var right *slice + s.left, right = root.cut(pos) + _, s.right = right.cut(pos + slen) + } + root = s + } + root.visit(func(s *slice) { + *chunks = append(*chunks, Slice{s.chunkid, s.cleng, s.off, s.len}) + }) + // TODO: compact + return 0 +} + +func (r *redisMeta) NewChunk(ctx Context, inode Ino, indx uint32, offset uint32, chunkid *uint64) syscall.Errno { + cid, err := r.rdb.Incr(c, "nextchunk").Uint64() + if err == nil { + *chunkid = cid + } + return errno(err) +} + +func (r *redisMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Slice) syscall.Errno { + return r.txn(func(tx *redis.Tx) error { + // TODO: refcount for chunkid + var attr Attr + a, err := tx.Get(c, r.inodeKey(inode)).Bytes() + if err != nil { + return err + } + r.parseAttr(a, &attr) + newleng := uint64(indx)*ChunkSize + uint64(off) + uint64(slice.Len) + var added int64 + if newleng > attr.Length { + added = align4K(newleng) - align4K(attr.Length) + attr.Length = newleng + } + now := time.Now() + attr.Mtime = now.Unix() + attr.Mtimensec = uint32(now.Nanosecond()) + attr.Ctime = now.Unix() + attr.Ctimensec = uint32(now.Nanosecond()) + + w := utils.NewBuffer(24) + w.Put32(off) + w.Put64(slice.Chunkid) + w.Put32(slice.Size) + w.Put32(slice.Off) + w.Put32(slice.Len) + + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.RPush(c, r.chunkKey(inode, indx), w.Bytes()) + pipe.Set(c, r.inodeKey(inode), r.marshal(&attr), 0) + if added > 0 { + pipe.IncrBy(c, usedSpace, added) + } + return nil + }) + return err + }, r.inodeKey(inode), r.chunkKey(inode, indx)) +} + +func (r *redisMeta) delChunks(inode Ino, start, end, maxchunkid uint64) string { + return fmt.Sprintf("%d:%d:%d:%d", inode, start, end, maxchunkid) +} + +func (r *redisMeta) cleanupChunks() { + for { + now := time.Now() + members, _ := r.rdb.ZRangeByScore(c, delchunks, &redis.ZRangeBy{strconv.Itoa(0), strconv.Itoa(int(now.Add(time.Hour).Unix())), 0, 1000}).Result() + for _, member := range members { + ps := strings.Split(member, ":") + if len(ps) != 4 { + logger.Errorf("invalid del chunks: %s", member) + continue + } + inode, _ := strconv.Atoi(ps[0]) + start, _ := strconv.Atoi(ps[1]) + end, _ := strconv.Atoi(ps[2]) + maxchunk, _ := strconv.Atoi(ps[3]) + r.deleteChunks(Ino(inode), uint64(start), uint64(end), uint64(maxchunk)) + } + time.Sleep(time.Minute) + } +} + +func (r *redisMeta) deleteChunks(inode Ino, start, end, maxchunk uint64) { + var i uint32 + if start > 0 { + i = uint32((start-1)/ChunkSize) + 1 + } + var rs []*redis.StringSliceCmd + for uint64(i)*ChunkSize <= end { + p := r.rdb.Pipeline() + var indx = i + for j := 0; uint64(i)*ChunkSize <= end && j < 1000; j++ { + rs = append(rs, p.LRange(c, r.chunkKey(inode, i), 0, 1000)) + i++ + } + vals, err := p.Exec(c) + if err != nil { + logger.Errorf("LRange %d[%d-%d]: %s", inode, start, end, err) + return + } + for j := range vals { + val, err := rs[j].Result() + if err == redis.Nil { + continue + } + for _, cs := range val { + rb := utils.ReadBuffer([]byte(cs)) + _ = rb.Get32() // pos + chunkid := rb.Get64() + if chunkid == 0 { + continue + } + // there could be new data written after the chunk is marked for deletion, + // so we should not delete any chunk with id > maxchunk + if chunkid > maxchunk { + // mark this chunk is deleted + break + } + cleng := rb.Get32() + err := r.newMsg(DeleteChunk, chunkid, cleng) + if err != nil { + logger.Warnf("delete chunk %d fail: %s, retry later", inode, err) + now := time.Now() + key := r.delChunks(inode, uint64((indx+uint32(j)))*ChunkSize, uint64((indx+uint32(j)+1))*ChunkSize, maxchunk) + r.rdb.ZAdd(c, delchunks, &redis.Z{float64(now.Unix()), key}) + return + } + r.txn(func(tx *redis.Tx) error { + val, err := tx.LRange(c, r.chunkKey(inode, indx+uint32(j)), 0, 1).Result() + if err != nil { + return err + } + if len(val) == 1 && val[0] == cs { + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.LPop(c, r.chunkKey(inode, indx+uint32(j))) + return nil + }) + return err + } + return nil + }, r.chunkKey(inode, indx+uint32(j))) + } + } + } + r.rdb.ZRem(c, delchunks, r.delChunks(inode, start, end, maxchunk)) +} + +func (r *redisMeta) GetXattr(ctx Context, inode Ino, name string, vbuff *[]byte) syscall.Errno { + var err error + *vbuff, err = r.rdb.HGet(c, r.xattrKey(inode), name).Bytes() + if err == redis.Nil { + err = syscall.ENODATA + } + return errno(err) +} + +func (r *redisMeta) ListXattr(ctx Context, inode Ino, names *[]byte) syscall.Errno { + vals, err := r.rdb.HKeys(c, r.xattrKey(inode)).Result() + if err != nil { + return errno(err) + } + *names = nil + for _, name := range vals { + *names = append(*names, []byte(name)...) + *names = append(*names, 0) + } + return 0 +} + +func (r *redisMeta) SetXattr(ctx Context, inode Ino, name string, value []byte) syscall.Errno { + _, err := r.rdb.HSet(c, r.xattrKey(inode), name, value).Result() + return errno(err) +} + +func (r *redisMeta) RemoveXattr(ctx Context, inode Ino, name string) syscall.Errno { + n, err := r.rdb.HDel(c, r.xattrKey(inode), name).Result() + if n == 0 { + err = syscall.ENODATA + } + return errno(err) +} + +func (r *redisMeta) Flock(ctx Context, inode Ino, owner uint64, ltype uint32, block bool) syscall.Errno { + lkey := r.ownerKey(owner) + if ltype == syscall.F_UNLCK { + _, err := r.rdb.HDel(c, r.flockKey(inode), lkey).Result() + return errno(err) + } + var err syscall.Errno + for { + err = r.txn(func(tx *redis.Tx) error { + owners, err := tx.HGetAll(c, r.flockKey(inode)).Result() + if err != nil { + return err + } + if ltype == syscall.F_RDLCK { + for _, v := range owners { + if v == "W" { + return syscall.EAGAIN + } + } + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.HSet(c, r.flockKey(inode), lkey, "R") + return nil + }) + return err + } + delete(owners, lkey) + if len(owners) > 0 { + return syscall.EAGAIN + } + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.HSet(c, r.flockKey(inode), lkey, "W") + return nil + }) + return err + }, r.flockKey(inode)) + + if !block || err != syscall.EAGAIN { + break + } + if ltype == syscall.F_WRLCK { + time.Sleep(time.Millisecond * 1) + } else { + time.Sleep(time.Millisecond * 10) + } + if ctx.Canceled() { + return syscall.EINTR + } + } + return err +} + +type plock struct { + ltype uint32 + pid uint32 + start uint64 + end uint64 +} + +func (r *redisMeta) loadLocks(d []byte) []plock { + var ls []plock + rb := utils.FromBuffer(d) + for rb.HasMore() { + ls = append(ls, plock{rb.Get32(), rb.Get32(), rb.Get64(), rb.Get64()}) + } + return ls +} + +func (r *redisMeta) dumpLocks(ls []plock) []byte { + wb := utils.NewBuffer(uint32(len(ls)) * 24) + for _, l := range ls { + wb.Put32(l.ltype) + wb.Put32(l.pid) + wb.Put64(l.start) + wb.Put64(l.end) + } + return wb.Bytes() +} + +func (r *redisMeta) insertLocks(ls []plock, i int, nl plock) []plock { + nls := make([]plock, len(ls)+1) + copy(nls[:i], ls[:i]) + nls[i] = nl + copy(nls[i+1:], ls[i:]) + ls = nls + return ls +} + +func (r *redisMeta) updateLocks(ls []plock, nl plock) []plock { + // ls is ordered by l.start without overlap + var i int + for i < len(ls) && nl.end > nl.start { + l := ls[i] + if l.end < nl.start { + } else if l.start < nl.start { + ls = r.insertLocks(ls, i+1, plock{nl.ltype, nl.pid, nl.start, l.end}) + ls[i].end = nl.start + i++ + nl.start = l.end + } else if l.end < nl.end { + ls[i].ltype = nl.ltype + ls[i].start = nl.start + nl.start = l.end + } else if l.start < nl.end { + ls = r.insertLocks(ls, i, nl) + ls[i+1].start = nl.end + nl.start = nl.end + } else { + ls = r.insertLocks(ls, i, nl) + nl.start = nl.end + } + i++ + } + if nl.start < nl.end { + ls = append(ls, nl) + } + i = 0 + for i < len(ls) { + if ls[i].ltype == syscall.F_UNLCK || ls[i].start == ls[i].end { + // remove empty one + copy(ls[i:], ls[i+1:]) + ls = ls[:len(ls)-1] + } else { + if i+1 < len(ls) && ls[i].ltype == ls[i+1].ltype && ls[i].end == ls[i+1].start { + // combine continous range + ls[i].end = ls[i+1].end + ls[i+1].start = ls[i+1].end + } + i++ + } + } + return ls +} + +func (r *redisMeta) Getlk(ctx Context, inode Ino, owner uint64, ltype *uint32, start, end *uint64, pid *uint32) syscall.Errno { + if *ltype == syscall.F_UNLCK { + *start = 0 + *end = 0 + *pid = 0 + return 0 + } + lkey := r.ownerKey(owner) + owners, err := r.rdb.HGetAll(c, r.plockKey(inode)).Result() + if err != nil { + return errno(err) + } + delete(owners, lkey) // exclude itself + for k, d := range owners { + ls := r.loadLocks([]byte(d)) + for _, l := range ls { + // find conflicted locks + if (*ltype == syscall.F_WRLCK || l.ltype == syscall.F_WRLCK) && *end > l.start && *start < l.end { + *ltype = l.ltype + *start = l.start + *end = l.end + sid, _ := strconv.Atoi(strings.Split(k, "_")[0]) + if int64(sid) == r.sid { + *pid = l.pid + } else { + *pid = 0 + } + return 0 + } + } + } + *ltype = syscall.F_UNLCK + *start = 0 + *end = 0 + *pid = 0 + return 0 +} + +func (r *redisMeta) Setlk(ctx Context, inode Ino, owner uint64, block bool, ltype uint32, start, end uint64, pid uint32) syscall.Errno { + lkey := r.ownerKey(owner) + var err syscall.Errno + lock := plock{ltype, pid, start, end} + for { + err = r.txn(func(tx *redis.Tx) error { + if ltype == syscall.F_UNLCK { + d, err := tx.HGet(c, r.plockKey(inode), lkey).Result() + if err != nil { + return err + } + ls := r.loadLocks([]byte(d)) + if len(ls) == 0 { + return nil + } + ls = r.updateLocks(ls, lock) + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + if len(ls) == 0 { + pipe.HDel(c, r.plockKey(inode), lkey) + } else { + pipe.HSet(c, r.plockKey(inode), lkey, r.dumpLocks(ls)) + } + return nil + }) + return err + } + owners, err := tx.HGetAll(c, r.plockKey(inode)).Result() + if err != nil { + return err + } + ls := r.loadLocks([]byte(owners[lkey])) + delete(owners, lkey) + for _, d := range owners { + ls := r.loadLocks([]byte(d)) + for _, l := range ls { + // find conflicted locks + if (ltype == syscall.F_WRLCK || l.ltype == syscall.F_WRLCK) && end > l.start && start < l.end { + return syscall.EAGAIN + } + } + } + ls = r.updateLocks(ls, lock) + _, err = tx.TxPipelined(c, func(pipe redis.Pipeliner) error { + pipe.HSet(c, r.plockKey(inode), lkey, r.dumpLocks(ls)) + return nil + }) + return err + }, r.plockKey(inode)) + + if !block || err != syscall.EAGAIN { + break + } + if ltype == syscall.F_WRLCK { + time.Sleep(time.Millisecond * 1) + } else { + time.Sleep(time.Millisecond * 10) + } + if ctx.Canceled() { + return syscall.EINTR + } + } + return err +} diff --git a/pkg/redis/redis_test.go b/pkg/redis/redis_test.go new file mode 100644 index 000000000000..4d97c233297a --- /dev/null +++ b/pkg/redis/redis_test.go @@ -0,0 +1,214 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package redis + +import ( + "sync" + "syscall" + "testing" + "time" + + "github.com/juicedata/juicefs/pkg/meta" +) + +func TestRedisClient(t *testing.T) { + var conf RedisConfig + m, err := NewRedisMeta("redis://127.0.0.1:6379/7", &conf) + if err != nil { + t.Logf("redis is not available: %s", err) + t.Skip() + } + m.OnMsg(meta.DeleteChunk, func(args ...interface{}) error { return nil }) + ctx := meta.Background + var parent, inode meta.Ino + var attr = &meta.Attr{} + m.GetAttr(ctx, 1, attr) // init + if st := m.Mkdir(ctx, 1, "d", 0640, 022, 0, &parent, attr); st != 0 { + t.Fatalf("mkdir %s", st) + } + defer m.Rmdir(ctx, 1, "d") + if st := m.Lookup(ctx, 1, "d", &parent, attr); st != 0 { + t.Fatalf("lookup dir: %s", st) + } + if st := m.Create(ctx, parent, "f", 0650, 022, &inode, attr); st != 0 { + t.Fatalf("create file %s", st) + } + defer m.Unlink(ctx, parent, "f") + if st := m.Lookup(ctx, parent, "f", &inode, attr); st != 0 { + t.Fatalf("lookup file: %s", st) + } + attr.Mtime = 2 + attr.Uid = 1 + if st := m.SetAttr(ctx, inode, meta.SetAttrMtime|meta.SetAttrUID, 0, attr); st != 0 { + t.Fatalf("setattr file %s", st) + } + if st := m.GetAttr(ctx, inode, attr); st != 0 { + t.Fatalf("getattr file %s", st) + } + if attr.Mtime != 2 || attr.Uid != 1 { + t.Fatalf("mtime:%d uid:%d", attr.Mtime, attr.Uid) + } + var entries []*meta.Entry + if st := m.Readdir(ctx, parent, 0, &entries); st != 0 { + t.Fatalf("readdir: %s", st) + } else if len(entries) != 1 { + t.Fatalf("entries: %d", len(entries)) + } + if st := m.Rename(ctx, parent, "f", 1, "f2", &inode, attr); st != 0 { + t.Fatalf("rename f %s", st) + } + defer m.Unlink(ctx, 1, "f2") + if st := m.Lookup(ctx, 1, "f2", &inode, attr); st != 0 { + t.Fatalf("lookup f2: %s", st) + } + + // data + var chunkid uint64 + if st := m.Open(ctx, inode, 2, attr); st != 0 { + t.Fatalf("open f2: %s", st) + } + if st := m.NewChunk(ctx, inode, 0, 0, &chunkid); st != 0 { + t.Fatalf("write chunk: %s", st) + } + var s = meta.Slice{chunkid, 100, 0, 100} + if st := m.Write(ctx, inode, 0, 100, s); st != 0 { + t.Fatalf("write end: %s", st) + } + var chunks []meta.Slice + if st := m.Read(inode, 0, &chunks); st != 0 { + t.Fatalf("read chunk: %s", st) + } + if len(chunks) != 1 || chunks[0].Chunkid != chunkid || chunks[0].Size != 100 { + t.Fatalf("chunks: %v", chunks) + } + if st := m.Fallocate(ctx, inode, fallocPunchHole|fallocKeepSize, 100, 50); st != 0 { + t.Fatalf("fallocate: %s", st) + } + if st := m.Read(inode, 0, &chunks); st != 0 { + t.Fatalf("read chunk: %s", st) + } + if len(chunks) != 3 || chunks[1].Chunkid != 0 || chunks[1].Len != 50 || chunks[2].Chunkid != chunkid || chunks[2].Len != 50 { + t.Fatalf("chunks: %v", chunks) + } + + // xattr + if st := m.SetXattr(ctx, inode, "a", []byte("v")); st != 0 { + t.Fatalf("setxattr: %s", st) + } + var value []byte + if st := m.GetXattr(ctx, inode, "a", &value); st != 0 || string(value) != "v" { + t.Fatalf("getxattr: %s %v", st, value) + } + if st := m.ListXattr(ctx, inode, &value); st != 0 || string(value) != "a\000" { + t.Fatalf("listxattr: %s %v", st, value) + } + if st := m.RemoveXattr(ctx, inode, "a"); st != 0 { + t.Fatalf("setxattr: %s", st) + } + + // flock + if st := m.Flock(ctx, inode, 1, syscall.F_RDLCK, false); st != 0 { + t.Fatalf("flock rlock: %s", st) + } + if st := m.Flock(ctx, inode, 2, syscall.F_RDLCK, false); st != 0 { + t.Fatalf("flock rlock: %s", st) + } + if st := m.Flock(ctx, inode, 1, syscall.F_WRLCK, false); st != syscall.EAGAIN { + t.Fatalf("flock wlock: %s", st) + } + if st := m.Flock(ctx, inode, 2, syscall.F_UNLCK, false); st != 0 { + t.Fatalf("flock unlock: %s", st) + } + if st := m.Flock(ctx, inode, 1, syscall.F_WRLCK, false); st != 0 { + t.Fatalf("flock wlock again: %s", st) + } + if st := m.Flock(ctx, inode, 2, syscall.F_WRLCK, false); st != syscall.EAGAIN { + t.Fatalf("flock wlock: %s", st) + } + if st := m.Flock(ctx, inode, 2, syscall.F_RDLCK, false); st != syscall.EAGAIN { + t.Fatalf("flock rlock: %s", st) + } + if st := m.Flock(ctx, inode, 1, syscall.F_UNLCK, false); st != 0 { + t.Fatalf("flock unlock: %s", st) + } + + // POSIX locks + if st := m.Setlk(ctx, inode, 1, false, syscall.F_RDLCK, 0, 0xFFFF, 1); st != 0 { + t.Fatalf("plock rlock: %s", st) + } + if st := m.Setlk(ctx, inode, 2, false, syscall.F_RDLCK, 0, 0x2FFFF, 1); st != 0 { + t.Fatalf("plock rlock: %s", st) + } + if st := m.Setlk(ctx, inode, 2, false, syscall.F_WRLCK, 0, 0xFFFF, 1); st != syscall.EAGAIN { + t.Fatalf("plock wlock: %s", st) + } + if st := m.Setlk(ctx, inode, 2, false, syscall.F_WRLCK, 0x10000, 0x20000, 1); st != 0 { + t.Fatalf("plock wlock: %s", st) + } + if st := m.Setlk(ctx, inode, 1, false, syscall.F_UNLCK, 0, 0x20000, 1); st != 0 { + t.Fatalf("plock unlock: %s", st) + } + if st := m.Setlk(ctx, inode, 2, false, syscall.F_WRLCK, 0, 0xFFFF, 10); st != 0 { + t.Fatalf("plock wlock: %s", st) + } + if st := m.Setlk(ctx, inode, 1, false, syscall.F_WRLCK, 0, 0xFFFF, 1); st != syscall.EAGAIN { + t.Fatalf("plock rlock: %s", st) + } + var ltype, pid uint32 = syscall.F_WRLCK, 1 + var start, end uint64 = 0, 0xFFFF + if st := m.Getlk(ctx, inode, 1, <ype, &start, &end, &pid); st != 0 || ltype != syscall.F_WRLCK || pid != 10 || start != 0 || end != 0xFFFF { + t.Fatalf("plock get rlock: %s, %d %d %x %x", st, ltype, pid, start, end) + } + if st := m.Setlk(ctx, inode, 2, false, syscall.F_UNLCK, 0, 0x2FFFF, 1); st != 0 { + t.Fatalf("plock unlock: %s", st) + } + ltype = syscall.F_WRLCK + start, end = 0, 0xFFFFFF + if st := m.Getlk(ctx, inode, 1, <ype, &start, &end, &pid); st != 0 || ltype != syscall.F_UNLCK || pid != 0 || start != 0 || end != 0 { + t.Fatalf("plock get rlock: %s, %d %d %x %x", st, ltype, pid, start, end) + } + + // concurrent locks + var g sync.WaitGroup + var count int + for i := 0; i < 100; i++ { + g.Add(1) + go func(i int) { + defer g.Done() + if st := m.Setlk(ctx, inode, uint64(i), true, syscall.F_WRLCK, 0, 0xFFFF, uint32(i)); st != 0 { + err = st + } + count++ + time.Sleep(time.Millisecond) + count-- + if count > 0 { + logger.Errorf("count should be be zero but got %d", count) + } + if st := m.Setlk(ctx, inode, uint64(i), false, syscall.F_UNLCK, 0, 0xFFFF, uint32(i)); st != 0 { + logger.Errorf("plock unlock: %s", st) + err = st + } + }(i) + } + g.Wait() + + if st := m.Unlink(ctx, 1, "f2"); st != 0 { + t.Fatalf("unlink: %s", st) + } + if st := m.Rmdir(ctx, 1, "d"); st != 0 { + t.Fatalf("rmdir: %s", st) + } +} diff --git a/pkg/redis/slice.go b/pkg/redis/slice.go new file mode 100644 index 000000000000..992e651aa40e --- /dev/null +++ b/pkg/redis/slice.go @@ -0,0 +1,77 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package redis + +type slice struct { + chunkid uint64 + cleng uint32 + off uint32 + len uint32 + pos uint32 + left *slice + right *slice +} + +func newSlice(pos uint32, chunkid uint64, cleng, off, len uint32) *slice { + if len == 0 { + return nil + } + s := &slice{} + s.pos = pos + s.chunkid = chunkid + s.cleng = cleng + s.off = off + s.len = len + s.left = nil + s.right = nil + return s +} + +func (s *slice) cut(pos uint32) (left, right *slice) { + if s == nil { + return nil, nil + } + if pos <= s.pos { + if s.left == nil { + s.left = newSlice(pos, 0, 0, 0, s.pos-pos) + } + left, s.left = s.left.cut(pos) + return left, s + } else if pos < s.pos+s.len { + l := pos - s.pos + right = newSlice(pos, s.chunkid, s.cleng, s.off+l, s.len-l) + right.right = s.right + s.len = l + s.right = nil + return s, right + } else { + if s.right == nil { + s.right = newSlice(s.pos+s.len, 0, 0, 0, pos-s.pos-s.len) + } + s.right, right = s.right.cut(pos) + return s, right + } +} + +func (s *slice) visit(f func(*slice)) { + if s == nil { + return + } + s.left.visit(f) + right := s.right + f(s) // s could be freed + right.visit(f) +} diff --git a/pkg/utils/alloc.go b/pkg/utils/alloc.go new file mode 100644 index 000000000000..f5ffec91561a --- /dev/null +++ b/pkg/utils/alloc.go @@ -0,0 +1,65 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package utils + +import ( + "runtime" + "sync" + "time" + "unsafe" +) + +var slabs = make(map[uintptr][]byte) +var used int64 +var slabsMutex sync.Mutex + +func Alloc(size int) []byte { + b := make([]byte, size) + ptr := unsafe.Pointer(&b[0]) + slabsMutex.Lock() + slabs[uintptr(ptr)] = b + used += int64(size) + slabsMutex.Unlock() + return b +} + +func Free(buf []byte) { + // buf could be zero when writing + p := unsafe.Pointer(&buf[:1][0]) + slabsMutex.Lock() + if b, ok := slabs[uintptr(p)]; !ok { + panic("invalid pointer") + } else { + used -= int64(len(b)) + } + delete(slabs, uintptr(p)) + slabsMutex.Unlock() +} + +func UsedMemory() int64 { + slabsMutex.Lock() + defer slabsMutex.Unlock() + return used +} + +func init() { + go func() { + for { + time.Sleep(time.Minute * 10) + runtime.GC() + } + }() +} diff --git a/pkg/utils/buffer.go b/pkg/utils/buffer.go new file mode 100644 index 000000000000..7a40dca309c6 --- /dev/null +++ b/pkg/utils/buffer.go @@ -0,0 +1,144 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package utils + +import ( + "encoding/binary" + "unsafe" +) + +type Buffer struct { + endian binary.ByteOrder + off int + buf []byte +} + +func NewBuffer(sz uint32) *Buffer { + return FromBuffer(make([]byte, sz)) +} + +func ReadBuffer(buf []byte) *Buffer { + return FromBuffer(buf) +} + +func FromBuffer(buf []byte) *Buffer { + return &Buffer{binary.BigEndian, 0, buf} +} + +func (b *Buffer) Len() int { + return len(b.buf) +} + +func (b *Buffer) HasMore() bool { + return b.off < len(b.buf) +} + +func (b *Buffer) Left() int { + return len(b.buf) - b.off +} + +func (b *Buffer) Seek(p int) { + b.off = p +} + +func (b *Buffer) Buffer() []byte { + return b.buf[b.off:] +} + +func (b *Buffer) Put8(v uint8) { + b.buf[b.off] = v + b.off++ +} + +func (b *Buffer) Get8() uint8 { + v := b.buf[b.off] + b.off++ + return v +} + +func (b *Buffer) Put16(v uint16) { + b.endian.PutUint16(b.buf[b.off:b.off+2], v) + b.off += 2 +} + +func (b *Buffer) Get16() uint16 { + v := b.endian.Uint16(b.buf[b.off : b.off+2]) + b.off += 2 + return v +} + +func (b *Buffer) Put32(v uint32) { + b.endian.PutUint32(b.buf[b.off:b.off+4], v) + b.off += 4 +} + +func (b *Buffer) Get32() uint32 { + v := b.endian.Uint32(b.buf[b.off : b.off+4]) + b.off += 4 + return v +} + +func (b *Buffer) Put64(v uint64) { + b.endian.PutUint64(b.buf[b.off:b.off+8], v) + b.off += 8 +} + +func (b *Buffer) Get64() uint64 { + v := b.endian.Uint64(b.buf[b.off : b.off+8]) + b.off += 8 + return v +} + +func (b *Buffer) Put(v []byte) { + l := len(v) + copy(b.buf[b.off:b.off+l], v) + b.off += l +} + +func (b *Buffer) Get(l int) []byte { + b.off += l + return b.buf[b.off-l : b.off] +} + +func (b *Buffer) SetBytes(buf []byte) { + b.endian = binary.BigEndian + b.off = 0 + b.buf = buf +} + +func (b *Buffer) Bytes() []byte { + return b.buf +} + +var nativeEndian binary.ByteOrder + +func NewNativeBuffer(buf []byte) *Buffer { + return &Buffer{nativeEndian, 0, buf} +} + +func init() { + buf := [2]byte{} + *(*uint16)(unsafe.Pointer(&buf[0])) = uint16(0xABCD) + + switch buf { + case [2]byte{0xCD, 0xAB}: + nativeEndian = binary.LittleEndian + case [2]byte{0xAB, 0xCD}: + nativeEndian = binary.BigEndian + default: + panic("Could not determine native endianness.") + } +} diff --git a/pkg/utils/buffer_test.go b/pkg/utils/buffer_test.go new file mode 100644 index 000000000000..acc9e0c45f75 --- /dev/null +++ b/pkg/utils/buffer_test.go @@ -0,0 +1,71 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package utils + +import ( + "fmt" + "testing" +) + +func assertEqual(t *testing.T, a interface{}, b interface{}) { + if a == b { + return + } + message := fmt.Sprintf("%v != %v", a, b) + t.Fatal(message) +} + +func TestBuffer(t *testing.T) { + b := NewBuffer(20) + b.Put8(1) + b.Put16(2) + b.Put32(3) + b.Put64(4) + b.Put([]byte("hello")) + + r := ReadBuffer(b.Bytes()) + assertEqual(t, r.Get8(), uint8(1)) + assertEqual(t, r.Get16(), uint16(2)) + assertEqual(t, r.Get32(), uint32(3)) + assertEqual(t, r.Get64(), uint64(4)) + assertEqual(t, string(r.Get(5)), "hello") +} + +func TestSetBytes(t *testing.T) { + var w Buffer + w.SetBytes(make([]byte, 3)) + w.Put8(1) + w.Put16(2) + r := ReadBuffer(w.Bytes()) + assertEqual(t, r.Get8(), uint8(1)) + assertEqual(t, r.Get16(), uint16(2)) +} + +func TestNativeBuffer(t *testing.T) { + b := NewNativeBuffer(make([]byte, 20)) + b.Put8(1) + b.Put16(2) + b.Put32(3) + b.Put64(4) + b.Put([]byte("hello")) + + r := NewNativeBuffer(b.Bytes()) + assertEqual(t, r.Get8(), uint8(1)) + assertEqual(t, r.Get16(), uint16(2)) + assertEqual(t, r.Get32(), uint32(3)) + assertEqual(t, r.Get64(), uint64(4)) + assertEqual(t, string(r.Get(5)), "hello") +} diff --git a/pkg/utils/compress.go b/pkg/utils/compress.go new file mode 100644 index 000000000000..a9b55c057baf --- /dev/null +++ b/pkg/utils/compress.go @@ -0,0 +1,102 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package utils + +import ( + "fmt" + "strings" + + "github.com/DataDog/zstd" + "github.com/hungys/go-lz4" +) + +const ZSTD_LEVEL = 1 // fastest + +type Compressor interface { + Name() string + CompressBound(int) int + Compress(dst, src []byte) (int, error) + Decompress(dst, src []byte) (int, error) +} + +func NewCompressor(algr string) Compressor { + algr = strings.ToLower(algr) + if algr == "zstd" { + return &ZStandard{ZSTD_LEVEL} + } else if algr == "lz4" { + return &LZ4{} + } else if algr == "none" || algr == "" { + return noOp{} + } + return nil +} + +type noOp struct{} + +func (n noOp) Name() string { return "Noop" } +func (n noOp) CompressBound(l int) int { return l } +func (n noOp) Compress(dst, src []byte) (int, error) { + if len(dst) < len(src) { + return 0, fmt.Errorf("buffer too short: %d < %d", len(dst), len(src)) + } + copy(dst, src) + return len(src), nil +} +func (n noOp) Decompress(dst, src []byte) (int, error) { + if len(dst) < len(src) { + return 0, fmt.Errorf("buffer too short: %d < %d", len(dst), len(src)) + } + copy(dst, src) + return len(src), nil +} + +type ZStandard struct { + level int +} + +func (n *ZStandard) Name() string { return "Zstd" } +func (n *ZStandard) CompressBound(l int) int { return zstd.CompressBound(l) } +func (n *ZStandard) Compress(dst, src []byte) (int, error) { + d, err := zstd.CompressLevel(dst, src, n.level) + if err != nil { + return 0, err + } + if len(d) > 0 && len(dst) > 0 && &d[0] != &dst[0] { + return 0, fmt.Errorf("buffer too short: %d < %d", cap(dst), cap(d)) + } + return len(d), err +} +func (n *ZStandard) Decompress(dst, src []byte) (int, error) { + d, err := zstd.Decompress(dst, src) + if err != nil { + return 0, err + } + if len(d) > 0 && len(dst) > 0 && &d[0] != &dst[0] { + return 0, fmt.Errorf("buffer too short: %d < %d", len(dst), len(d)) + } + return len(d), err +} + +type LZ4 struct{} + +func (l LZ4) Name() string { return "LZ4" } +func (l LZ4) CompressBound(size int) int { return lz4.CompressBound(size) } +func (l LZ4) Compress(dst, src []byte) (int, error) { + return lz4.CompressDefault(src, dst) +} +func (l LZ4) Decompress(dst, src []byte) (int, error) { + return lz4.DecompressSafe(src, dst) +} diff --git a/pkg/utils/compress_test.go b/pkg/utils/compress_test.go new file mode 100644 index 000000000000..43d8ebdb17b3 --- /dev/null +++ b/pkg/utils/compress_test.go @@ -0,0 +1,125 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package utils + +import ( + "io" + "os" + "testing" +) + +func testCompress(t *testing.T, c Compressor) { + src := []byte("hello") + dst := make([]byte, c.CompressBound(len(src))) + n, err := c.Compress(dst, src) + if err != nil { + t.Error(err) + t.FailNow() + } + src2 := make([]byte, len(src)) + n, err = c.Decompress(src2, dst[:n]) + if err != nil { + t.Error(err) + t.FailNow() + } + if string(src2[:n]) != string(src) { + t.Error("not matched", string(src2)) + t.FailNow() + } +} + +func TestUncompressed(t *testing.T) { + testCompress(t, NewCompressor("none")) +} + +func TestZstd(t *testing.T) { + testCompress(t, NewCompressor("zstd")) +} + +func benchmarkDecompress(b *testing.B, comp Compressor) { + f, _ := os.Open(os.Getenv("PAYLOAD")) + var c = make([]byte, 5<<20) + var d = make([]byte, 4<<20) + n, err := io.ReadFull(f, d) + f.Close() + if err != nil { + b.Skip() + return + } + d = d[:n] + n, err = comp.Compress(c[:4<<20], d) + if err != nil { + b.Errorf("compress: %s", err) + b.FailNow() + } + c = c[:n] + // println("compres", comp.Name(), len(c), len(d)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + n, err := comp.Decompress(d, c) + if err != nil { + b.Errorf("decompress %d %s", n, err) + b.FailNow() + } + b.SetBytes(int64(len(d))) + } +} + +func BenchmarkDecompressZstd(b *testing.B) { + benchmarkDecompress(b, NewCompressor("zstd")) +} + +func BenchmarkDecompressLZ4(b *testing.B) { + benchmarkDecompress(b, LZ4{}) +} + +func BenchmarkDecompressNone(b *testing.B) { + benchmarkDecompress(b, NewCompressor("none")) +} + +func benchmarkCompress(b *testing.B, comp Compressor) { + f, _ := os.Open(os.Getenv("PAYLOAD")) + var d = make([]byte, 4<<20) + n, err := io.ReadFull(f, d) + f.Close() + if err != nil { + b.Skip() + return + } + d = d[:n] + var c = make([]byte, 5<<20) + // println("compres", comp.Name(), len(c), len(d)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + n, err := comp.Compress(c, d) + if err != nil { + b.Errorf("compress %d %s", n, err) + b.FailNow() + } + b.SetBytes(int64(len(d))) + } +} + +func BenchmarkCompressZstd(b *testing.B) { + benchmarkCompress(b, NewCompressor("Zstd")) +} + +func BenchmarkCompressCLZ4(b *testing.B) { + benchmarkCompress(b, LZ4{}) +} +func BenchmarkCompressNone(b *testing.B) { + benchmarkCompress(b, NewCompressor("none")) +} diff --git a/pkg/utils/cond.go b/pkg/utils/cond.go new file mode 100644 index 000000000000..646bf41833b3 --- /dev/null +++ b/pkg/utils/cond.go @@ -0,0 +1,76 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package utils + +import ( + "sync" + "time" +) + +type Cond struct { + L sync.Locker + signal chan bool +} + +func (c *Cond) Signal() { + select { + case c.signal <- true: + default: + } +} + +func (c *Cond) Broadcast() { + for { + select { + case c.signal <- true: + default: + return + } + } +} + +func (c *Cond) Wait() { + c.L.Unlock() + defer c.L.Lock() + <-c.signal +} + +var timerPool = sync.Pool{ + New: func() interface{} { + return time.NewTimer(time.Second) + }, +} + +func (c *Cond) WaitWithTimeout(d time.Duration) bool { + c.L.Unlock() + t := timerPool.Get().(*time.Timer) + t.Reset(d) + defer func() { + t.Stop() + timerPool.Put(t) + }() + defer c.L.Lock() + select { + case <-c.signal: + return false + case <-t.C: + return true + } +} + +func NewCond(lock sync.Locker) *Cond { + return &Cond{lock, make(chan bool, 1)} +} diff --git a/pkg/utils/logger.go b/pkg/utils/logger.go new file mode 100644 index 000000000000..f9fa6942eab4 --- /dev/null +++ b/pkg/utils/logger.go @@ -0,0 +1,108 @@ +// Copyright 2015 Ka-Hing Cheung +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "fmt" + glog "log" + "os" + "strings" + "sync" + + "github.com/sirupsen/logrus" +) + +var mu sync.Mutex +var loggers = make(map[string]*logHandle) + +var syslogHook logrus.Hook + +type logHandle struct { + logrus.Logger + + name string + lvl *logrus.Level +} + +func (l *logHandle) Format(e *logrus.Entry) ([]byte, error) { + // Mon Jan 2 15:04:05 -0700 MST 2006 + timestamp := "" + lvl := e.Level + if l.lvl != nil { + lvl = *l.lvl + } + + const timeFormat = "2006/01/02 15:04:05.000000" + timestamp = e.Time.Format(timeFormat) + + str := fmt.Sprintf("%v %s[%d] <%v>: %v", + timestamp, + l.name, + os.Getpid(), + strings.ToUpper(lvl.String()), + e.Message) + + if len(e.Data) != 0 { + str += " " + fmt.Sprint(e.Data) + } + + str += "\n" + return []byte(str), nil +} + +// for aws.Logger +func (l *logHandle) Log(args ...interface{}) { + l.Debugln(args...) +} + +func NewLogger(name string) *logHandle { + l := &logHandle{name: name} + l.Out = os.Stderr + l.Formatter = l + l.Level = logrus.InfoLevel + l.Hooks = make(logrus.LevelHooks) + if syslogHook != nil { + l.Hooks.Add(syslogHook) + } + return l +} + +func GetLogger(name string) *logHandle { + mu.Lock() + defer mu.Unlock() + + if logger, ok := loggers[name]; ok { + return logger + } + logger := NewLogger(name) + loggers[name] = logger + return logger +} + +func GetStdLogger(l *logHandle, lvl logrus.Level) *glog.Logger { + mu.Lock() + defer mu.Unlock() + + w := l.Writer() + l.Formatter.(*logHandle).lvl = &lvl + l.Level = lvl + return glog.New(w, "", 0) +} + +func SetLogLevel(lvl logrus.Level) { + for _, logger := range loggers { + logger.Level = lvl + } +} diff --git a/pkg/utils/logger_syslog.go b/pkg/utils/logger_syslog.go new file mode 100644 index 000000000000..0c23def5c98f --- /dev/null +++ b/pkg/utils/logger_syslog.go @@ -0,0 +1,72 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package utils + +import ( + "fmt" + "log/syslog" + "os" + + "github.com/sirupsen/logrus" + logrus_syslog "github.com/sirupsen/logrus/hooks/syslog" +) + +type SyslogHook struct { + *logrus_syslog.SyslogHook +} + +func (hook *SyslogHook) Fire(entry *logrus.Entry) error { + line, err := entry.String() + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err) + return err + } + + // drop the timestamp + line = line[27:] + + switch entry.Level { + case logrus.PanicLevel: + return hook.Writer.Crit(line) + case logrus.FatalLevel: + return hook.Writer.Crit(line) + case logrus.ErrorLevel: + return hook.Writer.Err(line) + case logrus.WarnLevel: + return hook.Writer.Warning(line) + case logrus.InfoLevel: + return hook.Writer.Info(line) + case logrus.DebugLevel: + return hook.Writer.Debug(line) + default: + return nil + } +} + +func InitLoggers(logToSyslog bool) { + if logToSyslog { + hook, err := logrus_syslog.NewSyslogHook("", "", syslog.LOG_DEBUG|syslog.LOG_USER, "") + if err != nil { + // println("Unable to connect to local syslog daemon") + return + } + syslogHook = &SyslogHook{hook} + + for _, l := range loggers { + l.Hooks.Add(syslogHook) + } + } +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 000000000000..c9f4fab2b2cb --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,51 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package utils + +import ( + "io" + "os" +) + +func Min(a, b int) int { + if a < b { + return a + } + return b +} + +func Exists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func CopyFile(dst, src string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, in) + if err != nil { + return err + } + return out.Close() +} diff --git a/pkg/vfs/accesslog.go b/pkg/vfs/accesslog.go new file mode 100644 index 000000000000..724852c7242b --- /dev/null +++ b/pkg/vfs/accesslog.go @@ -0,0 +1,100 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package vfs + +import ( + "fmt" + "sync" + "time" +) + +const ( + maxLineLength = 1000 +) + +var ( + readerLock sync.Mutex + readers map[uint64]chan []byte +) + +func init() { + readers = make(map[uint64]chan []byte) +} + +func logit(ctx Context, format string, args ...interface{}) { + used := ctx.Duration() + readerLock.Lock() + defer readerLock.Unlock() + if len(readers) == 0 || used > time.Second*10 { + return + } + + cmd := fmt.Sprintf(format, args...) + t := time.Now() + ts := t.Format("2006.01.02 15:04:05.000000") + cmd += fmt.Sprintf(" <%.6f>", used.Seconds()) + if ctx.Pid() != 0 && used > time.Second*10 { + logger.Infof("slow operation: %s", cmd) + } + line := []byte(fmt.Sprintf("%s [uid:%d,gid:%d,pid:%d] %s\n", ts, ctx.Uid(), ctx.Gid(), ctx.Pid(), cmd)) + + for _, ch := range readers { + select { + case ch <- line: + default: + } + } +} + +func openAccessLog(fh uint64) uint64 { + readerLock.Lock() + defer readerLock.Unlock() + readers[fh] = make(chan []byte, 1024) + return fh +} + +func closeAccessLog(fh uint64) { + readerLock.Lock() + defer readerLock.Unlock() + delete(readers, fh) +} + +func readAccessLog(fh uint64, buf []byte) int { + readerLock.Lock() + buffer, ok := readers[fh] + readerLock.Unlock() + if !ok { + return 0 + } + var n int + var t = time.NewTimer(time.Second) + select { + case l := <-buffer: + n = copy(buf, l) + for n+maxLineLength <= len(buf) { + select { + case l = <-buffer: + n += copy(buf[n:], l) + default: + return n + } + } + return n + case <-t.C: + n = copy(buf, []byte("#\n")) + } + return n +} diff --git a/pkg/vfs/handle.go b/pkg/vfs/handle.go new file mode 100644 index 000000000000..6a56f4e02e92 --- /dev/null +++ b/pkg/vfs/handle.go @@ -0,0 +1,226 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package vfs + +import ( + "sync" + "time" + + "github.com/juicedata/juicefs/pkg/meta" + "github.com/juicedata/juicefs/pkg/utils" +) + +type handle struct { + sync.Mutex + inode Ino + fh uint64 + + // for dir + children []*meta.Entry + + // for file + length uint64 + mode uint8 + locks uint8 + flockOwner uint64 // kernel 3.1- does not pass lock_owner in release() + reader FileReader + writer FileWriter + ops []Context + + // rwlock + writing uint32 + readers uint32 + writers uint32 + cond *utils.Cond + + // internal files + data []byte +} + +func (h *handle) addOp(ctx Context) { + h.ops = append(h.ops, ctx) +} + +func (h *handle) removeOp(ctx Context) { + h.Lock() + for i, c := range h.ops { + if c == ctx { + h.ops[i] = h.ops[len(h.ops)-1] + h.ops = h.ops[:len(h.ops)-1] + break + } + } + h.Unlock() +} + +func (h *handle) cancelOp(pid uint32) { + if pid == 0 { + return + } + for _, c := range h.ops { + if c.Pid() == pid || c.Pid() > 0 && c.Duration() > time.Second { + c.Cancel() + } + } +} + +func (h *handle) Rlock(ctx Context) bool { + h.Lock() + for (h.writing | h.writers) != 0 { + if h.cond.WaitWithTimeout(time.Millisecond*100) && ctx.Canceled() { + h.Unlock() + return false + } + } + h.readers++ + if h.reader == nil { + h.reader = reader.Open(h.inode, h.length) + } + h.addOp(ctx) + h.Unlock() + return true +} + +func (h *handle) Runlock() { + h.Lock() + h.readers-- + if h.readers == 0 { + h.cond.Broadcast() + } + h.Unlock() +} + +func (h *handle) Wlock(ctx Context) bool { + h.Lock() + h.writers++ + for (h.readers | h.writing) != 0 { + if h.cond.WaitWithTimeout(time.Millisecond*100) && ctx.Canceled() { + h.writers-- + h.Unlock() + return false + } + } + h.writers-- + h.writing = 1 + if h.writer == nil { + h.writer = writer.Open(h.inode, h.length) + } + h.addOp(ctx) + h.Unlock() + return true +} + +func (h *handle) Wunlock() { + h.Lock() + h.writing = 0 + h.cond.Broadcast() + h.Unlock() +} + +func (h *handle) Close() { + if h.reader != nil { + h.reader.Close(meta.Background) + } + if h.writer != nil { + h.writer.Close(meta.Background) + } +} + +var ( + handles map[Ino][]*handle + hanleLock sync.Mutex + nextfh uint64 = 1 +) + +func newHandle(inode Ino) *handle { + hanleLock.Lock() + defer hanleLock.Unlock() + fh := nextfh + h := &handle{inode: inode, fh: fh} + nextfh++ + h.cond = utils.NewCond(h) + handles[inode] = append(handles[inode], h) + return h +} + +func findHandle(inode Ino, fh uint64) *handle { + hanleLock.Lock() + defer hanleLock.Unlock() + for _, f := range handles[inode] { + if f.fh == fh { + return f + } + } + return nil +} + +func releaseHandle(inode Ino, fh uint64) { + hanleLock.Lock() + defer hanleLock.Unlock() + hs := handles[inode] + for i, f := range hs { + if f.fh == fh { + if i+1 < len(hs) { + hs[i] = hs[len(hs)-1] + } + if len(hs) > 1 { + handles[inode] = hs[:len(hs)-1] + } else { + delete(handles, inode) + } + break + } + } +} + +func updateHandleLength(inode Ino, length uint64) { + hanleLock.Lock() + for _, h := range handles[inode] { + h.Lock() + h.length = length + h.Unlock() + } + hanleLock.Unlock() +} + +func newFileHandle(mode uint8, inode Ino, length uint64) uint64 { + h := newHandle(inode) + h.Lock() + defer h.Unlock() + h.length = length + h.mode = mode + if mode == modeRead { + h.reader = reader.Open(inode, length) + } else if mode == modeWrite { + h.writer = writer.Open(inode, length) + } + return h.fh +} + +func releaseFileHandle(ino Ino, fh uint64) { + h := findHandle(ino, fh) + if h != nil { + h.Lock() + // rwlock_wait_for_unlock: + for (h.writing | h.writers | h.readers) != 0 { + h.cond.WaitWithTimeout(time.Millisecond * 100) + } + h.writing = 1 // for remove + h.Unlock() + h.Close() + releaseHandle(ino, fh) + } +} diff --git a/pkg/vfs/helpers.go b/pkg/vfs/helpers.go new file mode 100644 index 000000000000..224ebdbd8f62 --- /dev/null +++ b/pkg/vfs/helpers.go @@ -0,0 +1,102 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package vfs + +import ( + "fmt" + "syscall" + "time" + + "github.com/juicedata/juicefs/pkg/meta" +) + +func strerr(errno syscall.Errno) string { + if errno == 0 { + return "OK" + } + return errno.Error() +} + +var typestr = map[uint16]byte{ + syscall.S_IFSOCK: 's', + syscall.S_IFLNK: 'l', + syscall.S_IFREG: '-', + syscall.S_IFBLK: 'b', + syscall.S_IFDIR: 'd', + syscall.S_IFCHR: 'c', + syscall.S_IFIFO: 'f', + 0: '?', +} + +type smode uint16 + +func (mode smode) String() string { + s := []byte("?rwxrwxrwx") + s[0] = typestr[uint16(mode)&(syscall.S_IFMT&0xffff)] + if (mode & syscall.S_ISUID) != 0 { + s[3] = 's' + } + if (mode & syscall.S_ISGID) != 0 { + s[6] = 's' + } + if (mode & syscall.S_ISVTX) != 0 { + s[9] = 't' + } + for i := uint16(0); i < 9; i++ { + if (mode & (1 << i)) == 0 { + if s[9-i] == 's' || s[9-i] == 't' { + s[9-i] &= 0xDF + } else { + s[9-i] = '-' + } + } + } + return string(s) +} + +type Entry meta.Entry + +func (entry *Entry) String() string { + if entry == nil { + return "" + } + if entry.Attr == nil { + return fmt.Sprintf(" (%d)", entry.Inode) + } + a := entry.Attr + mode := a.SMode() + return fmt.Sprintf(" (%d,[%s:0%06o,%d,%d,%d,%d,%d,%d,%d])", + entry.Inode, smode(mode), mode, a.Nlink, a.Uid, a.Gid, + a.Atime, a.Mtime, a.Ctime, a.Length) +} + +type LogContext interface { + meta.Context + Duration() time.Duration +} + +type logContext struct { + meta.Context + start time.Time +} + +func (ctx *logContext) Duration() time.Duration { + return time.Since(ctx.start) +} + +func NewLogContext(ctx meta.Context) LogContext { + return &logContext{ctx, time.Now()} +} diff --git a/pkg/vfs/internal.go b/pkg/vfs/internal.go new file mode 100644 index 000000000000..0df044aa5fdc --- /dev/null +++ b/pkg/vfs/internal.go @@ -0,0 +1,90 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package vfs + +import ( + "os" + "time" + + "github.com/juicedata/juicefs/pkg/meta" +) + +const ( + minInternalNode = 0x7FFFFFFFFFFFF0 + logInode = minInternalNode + 1 +) + +type internalNode struct { + inode Ino + name string + attr *Attr +} + +var internalNodes = []*internalNode{ + {logInode, ".accesslog", &Attr{Mode: 0400}}, +} + +func init() { + uid := uint32(os.Getuid()) + gid := uint32(os.Getgid()) + now := time.Now().Unix() + for _, v := range internalNodes { + v.attr.Typ = meta.TypeFile + v.attr.Uid = uid + v.attr.Gid = gid + v.attr.Atime = now + v.attr.Mtime = now + v.attr.Ctime = now + v.attr.Nlink = 1 + } +} + +func IsSpecialNode(ino Ino) bool { + return ino >= minInternalNode +} + +func isSpecialName(name string) bool { + if name[0] != '.' { + return false + } + for _, n := range internalNodes { + if name == n.name { + return true + } + } + return false +} + +func getInternalNode(ino Ino) *internalNode { + for _, n := range internalNodes { + if ino == n.inode { + return n + } + } + return nil +} + +func getInternalNodeByName(name string) *internalNode { + if name[0] != '.' { + return nil + } + for _, n := range internalNodes { + if name == n.name { + return n + } + } + return nil +} diff --git a/pkg/vfs/reader.go b/pkg/vfs/reader.go new file mode 100644 index 000000000000..643d5b52736f --- /dev/null +++ b/pkg/vfs/reader.go @@ -0,0 +1,831 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package vfs + +import ( + "context" + "fmt" + "runtime" + "sort" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/juicedata/juicefs/pkg/chunk" + "github.com/juicedata/juicefs/pkg/meta" + "github.com/juicedata/juicefs/pkg/utils" +) + +/* + * state of sliceReader + * + * <-- REFRESH + * | | + * NEW -> BUSY -> READY + * | | + * BREAK ---> INVALID + */ +const ( + NEW = iota + BUSY + REFRESH + BREAK + READY + INVALID +) + +const readSessions = 2 + +var readBufferUsed int64 + +type sstate uint8 + +func (m sstate) valid() bool { return m != BREAK && m != INVALID } + +var stateNames = []string{"NEW", "BUSY", "REFRESH", "BREAK", "READY", "INVALID"} + +func (m sstate) String() string { + if m <= INVALID { + return stateNames[m] + } + panic("") +} + +type FileReader interface { + Read(ctx meta.Context, off uint64, buf []byte) (int, syscall.Errno) + Close(ctx meta.Context) +} + +type DataReader interface { + Open(inode Ino, fleng uint64) FileReader +} + +type frange struct { + off uint64 + len uint64 +} + +func (r *frange) String() string { return fmt.Sprintf("[%d,%d,%d)", r.off, r.len, r.end()) } +func (r *frange) end() uint64 { return r.off + r.len } +func (r *frange) contain(p uint64) bool { return r.off < p && p < r.end() } +func (r *frange) overlap(a *frange) bool { return a.off < r.end() && r.off < a.end() } +func (r *frange) include(a *frange) bool { return r.off <= a.off && a.end() <= r.end() } + +// protected by file +type sliceReader struct { + file *fileReader + indx uint32 + block *frange + state sstate + page *chunk.Page + need uint64 + currentpos uint32 + modified time.Time + refs uint16 + cond *utils.Cond + next *sliceReader + prev **sliceReader +} + +func (s *sliceReader) delay(delay time.Duration) { + time.AfterFunc(delay, s.run) +} + +func (s *sliceReader) done(err syscall.Errno, delay time.Duration) { + f := s.file + switch s.state { + case BUSY: + s.state = NEW // failed + case BREAK: + s.state = INVALID + case REFRESH: + s.state = NEW + } + if err != 0 { + if !f.closing { + logger.Errorf("read file %d: %s", f.inode, err) + } + f.err = err + } + if f.shouldStop() { + s.state = INVALID + } + + switch s.state { + case NEW: + s.delay(delay) + case READY: + s.cond.Broadcast() + case INVALID: + if s.refs == 0 { + s.delete() + if f.closing && f.slices == nil { + f.r.Lock() + if f.refs == 0 { + f.delete() + } + f.r.Unlock() + } + } else { + s.cond.Broadcast() + } + } + runtime.Goexit() +} + +func retry_time(trycnt uint32) time.Duration { + if trycnt < 30 { + return time.Millisecond * time.Duration((trycnt-1)*300+1) + } + return time.Second * 10 +} + +func (s *sliceReader) run() { + f := s.file + f.Lock() + defer f.Unlock() + if s.state != NEW || f.shouldStop() { + s.done(0, 0) + } + s.state = BUSY + indx := s.indx + inode := f.inode + f.Unlock() + + f.Lock() + length := f.length + f.Unlock() + var chunks []meta.Slice + err := f.r.m.Read(inode, indx, &chunks) + f.Lock() + if s.state != BUSY || f.err != 0 || f.closing { + s.done(0, 0) + } + if err == syscall.ENOENT { + s.done(err, 0) + } else if err != 0 { + f.tried++ + trycnt := f.tried + if trycnt >= f.r.maxRetries { + s.done(syscall.EIO, 0) + } else { + s.done(0, retry_time(trycnt)) + } + } + + s.currentpos = 0 + if s.block.off > length { + s.need = 0 + s.state = READY + s.done(0, 0) + } else if s.block.end() > length { + s.need = length - s.block.off + } else { + s.need = s.block.len + } + need := s.need + f.Unlock() + + p := s.page.Slice(0, int(need)) + defer p.Release() + var n int + ctx := context.TODO() + n = f.r.Read(ctx, p, chunks, (uint32(s.block.off))%meta.ChunkSize) + + f.Lock() + if s.state != BUSY || f.shouldStop() { + s.done(0, 0) + } + if n == int(need) { + s.state = READY + s.currentpos = uint32(n) + s.file.tried = 0 + s.modified = time.Now() + s.done(0, 0) + } else { + s.currentpos = 0 // start again from beginning + err = syscall.EIO + f.tried++ + // ind.r.m.InvalidateChunkCache(inode, chindx) + if f.tried >= f.r.maxRetries { + s.done(err, 0) + } else { + s.done(0, retry_time(f.tried)) + } + } +} + +func (s *sliceReader) invalidate() { + switch s.state { + case NEW: + case BUSY: + s.state = REFRESH + // TODO: interrupt reader + case READY: + if s.refs > 0 { + s.state = NEW + go s.run() + } else { + s.state = INVALID + s.delete() // nobody wants it anymore, so delete it + } + } +} + +func (s *sliceReader) drop() { + if s.state <= BREAK { + if s.refs == 0 { + s.state = BREAK + // TODO: interrupt reader + } + } else { + if s.refs == 0 { + s.delete() // nobody wants it anymore, so delete it + } else if s.state == READY { + s.state = INVALID // somebody still using it, so mark it for removal + } + } +} + +func (s *sliceReader) delete() { + *(s.prev) = s.next + if s.next != nil { + s.next.prev = s.prev + } else { + s.file.last = s.prev + } + s.page.Release() + atomic.AddInt64(&readBufferUsed, -int64(s.block.len)) +} + +type session struct { + lastOffset uint64 + total uint64 + readahead uint64 + atime time.Time +} + +type fileReader struct { + // protected by itself + inode Ino + length uint64 + err syscall.Errno + tried uint32 + sessions [readSessions]session + slices *sliceReader + last **sliceReader + + sync.Mutex + closing bool + + // protected by r + refs uint16 + next *fileReader + r *dataReader +} + +func (f *fileReader) newSlice(block *frange) *sliceReader { + s := &sliceReader{} + s.file = f + s.modified = time.Now() + s.indx = uint32(block.off / meta.ChunkSize) + s.block = &frange{block.off, block.len} // random read + blockend := (block.off/f.r.blockSize + 1) * f.r.blockSize + if s.block.end() > f.length { + s.block.len = f.length - s.block.off + } + if s.block.end() > blockend { + s.block.len = blockend - s.block.off + } + block.off = s.block.end() + block.len -= s.block.len + s.page = chunk.NewOffPage(int(s.block.len)) + s.cond = utils.NewCond(&f.Mutex) + s.prev = f.last + *(f.last) = s + f.last = &(s.next) + go s.run() + atomic.AddInt64(&readBufferUsed, int64(s.block.len)) + return s +} + +func (f *fileReader) delete() { + r := f.r + i := r.files[f.inode] + if i == f { + if i.next != nil { + r.files[f.inode] = i.next + } else { + delete(r.files, f.inode) + } + } else { + for i != nil { + if i.next == f { + i.next = f.next + break + } + i = i.next + } + } +} + +func (f *fileReader) acquire() { + f.r.Lock() + defer f.r.Unlock() + f.refs++ +} + +func (f *fileReader) release() { + f.r.Lock() + defer f.r.Unlock() + f.refs-- + if f.refs == 0 && f.slices == nil { + f.delete() + } +} + +func (f *fileReader) guessSession(block *frange) int { + idx := -1 + var closestOff uint64 + for i, ses := range f.sessions { + if ses.lastOffset > closestOff && ses.lastOffset <= block.off && block.off <= ses.lastOffset+ses.readahead+f.r.blockSize { + idx = i + closestOff = ses.lastOffset + } + } + if idx == -1 { + for i, ses := range f.sessions { + bt := ses.readahead / 8 + if bt < f.r.blockSize { + bt = f.r.blockSize + } + min := ses.lastOffset - bt + if ses.lastOffset < bt { + min = 0 + } + if min <= block.off && block.off < ses.lastOffset && (closestOff == 0 || ses.lastOffset < closestOff) { + idx = i + closestOff = ses.lastOffset + } + } + } + if idx == -1 { + for i, ses := range f.sessions { + if ses.total == 0 { + idx = i + break + } + if idx == -1 || ses.atime.Before(f.sessions[idx].atime) { + idx = i + } + } + f.sessions[idx].lastOffset = block.off + f.sessions[idx].total = block.len + f.sessions[idx].readahead = 0 + } else { + if block.end() > f.sessions[idx].lastOffset { + f.sessions[idx].total += block.end() - f.sessions[idx].lastOffset + } + } + f.sessions[idx].atime = time.Now() + return idx +} + +func (f *fileReader) checkReadahead(block *frange) int { + idx := f.guessSession(block) + ses := &f.sessions[idx] + seqdata := ses.total + readahead := ses.readahead + used := uint64(atomic.LoadInt64(&readBufferUsed)) + if readahead == 0 && (block.off == 0 || seqdata > block.len) { // begin with read-ahead turned on + ses.readahead = f.r.blockSize + } else if readahead < f.r.readAheadMax && seqdata >= readahead && f.r.readAheadTotal-used > readahead*4 { + ses.readahead *= 2 + } else if readahead >= f.r.blockSize && (f.r.readAheadTotal-used < readahead/2 || seqdata < readahead/4) { + ses.readahead /= 2 + } + if ses.readahead >= f.r.blockSize { + ahead := frange{block.end(), ses.readahead} + f.readAhead(&ahead) + } + if block.end() > ses.lastOffset { + ses.lastOffset = block.end() + } + return idx +} + +func (f *fileReader) need(block *frange) bool { + for _, ses := range f.sessions { + if ses.total == 0 { + break + } + bt := ses.readahead / 8 + if bt < f.r.blockSize { + bt = f.r.blockSize + } + b := &frange{ses.lastOffset - bt, ses.readahead*2 + f.r.blockSize*2} + if ses.lastOffset < bt { + b.off = 0 + } + if block.overlap(b) { + return true + } + } + return false +} + +// cleanup unused requests +func (f *fileReader) cleanupRequests(block *frange) { + now := time.Now() + var cnt int + f.visit(func(s *sliceReader) { + if !s.state.valid() || + !block.overlap(s.block) && (s.modified.Add(time.Second*30).Before(now) || !f.need(s.block)) { + s.drop() + } else if !block.overlap(s.block) { + cnt++ + } + }) + f.visit(func(s *sliceReader) { + if !block.overlap(s.block) && cnt > f.r.maxRequests { + s.drop() + cnt-- + } + }) +} + +type uint64Slice []uint64 + +func (p uint64Slice) Len() int { return len(p) } +func (p uint64Slice) Less(i, j int) bool { return p[i] < p[j] } +func (p uint64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +func (f *fileReader) splitRange(block *frange) []uint64 { + ranges := []uint64{block.off, block.end()} + contain := func(p uint64) bool { + for _, i := range ranges { + if i == p { + return true + } + } + return false + } + f.visit(func(s *sliceReader) { + if s.state.valid() { + if block.contain(s.block.off) && !contain(s.block.off) { + ranges = append(ranges, s.block.off) + } + if block.contain(s.block.end()) && !contain(s.block.end()) { + ranges = append(ranges, s.block.end()) + } + } + }) + sort.Sort(uint64Slice(ranges)) + return ranges +} + +func (f *fileReader) readAhead(block *frange) { + f.visit(func(r *sliceReader) { + if r.state.valid() && r.block.off <= block.off && r.block.end() > block.off { + if r.state == READY && block.len > f.r.blockSize && r.block.off == block.off && r.block.off%f.r.blockSize == 0 { + // next block is ready, reduce readahead by a block + block.len -= f.r.blockSize / 2 + } + if r.block.end() <= block.end() { + block.len = block.end() - r.block.end() + } else { + block.len = 0 + } + block.off = r.block.end() + } + }) + if block.len > 0 && block.off < f.length && uint64(atomic.LoadInt64(&readBufferUsed)) < f.r.readAheadTotal { + if block.len < f.r.blockSize { + block.len += f.r.blockSize - block.end()%f.r.blockSize // align to end of a block + } + f.newSlice(block) + if block.len > 0 { + f.readAhead(block) + } + } +} + +type req struct { + frange + s *sliceReader +} + +func (f *fileReader) prepareRequests(ranges []uint64) []*req { + var reqs []*req + edges := len(ranges) + for i := 0; i < edges-1; i++ { + var added bool + b := frange{ranges[i], ranges[i+1] - ranges[i]} + f.visit(func(s *sliceReader) { + if !added && s.state.valid() && s.block.include(&b) { + s.refs++ + reqs = append(reqs, &req{frange{ranges[i] - s.block.off, b.len}, s}) + added = true + } + }) + if !added { + for b.len > 0 { + s := f.newSlice(&b) + s.refs++ + reqs = append(reqs, &req{frange{0, s.block.len}, s}) + } + } + } + return reqs +} + +func (f *fileReader) shouldStop() bool { + return f.err != 0 || f.closing +} + +func (f *fileReader) waitForIO(ctx meta.Context, reqs []*req, buf []byte) (int, syscall.Errno) { + for _, req := range reqs { + s := req.s + for s.state != READY && uint64(s.currentpos) < req.end() { + if s.cond.WaitWithTimeout(time.Millisecond * 10) { + if ctx.Canceled() { + return 0, syscall.EINTR + } + } + if f.shouldStop() { + return 0, f.err + } + } + if s.need < s.block.len { + break // short read + } + } + + var shortRead = false + var n int + for _, req := range reqs { + s := req.s + if req.off < s.need && s.block.off+req.off < f.length { + if req.end() > s.need { + req.len = s.need - req.off + shortRead = true + } + if s.block.off+req.end() > f.length { + req.len = f.length - s.block.off - req.off + } + n += copy(buf[n:], s.page.Data[req.off:req.end()]) + } + if shortRead { + break + } + } + return n, 0 +} + +func (f *fileReader) Read(ctx meta.Context, offset uint64, buf []byte) (int, syscall.Errno) { + f.Lock() + defer f.Unlock() + f.acquire() + defer f.release() + + if f.err != 0 || f.closing { + return 0, f.err + } + + size := uint32(len(buf)) + if offset >= f.length || size == 0 { + return 0, 0 + } + block := &frange{offset, uint64(size)} + if block.end() > f.length { + block.len = f.length - block.off + } + + f.cleanupRequests(block) + var lastBS uint64 = 32 << 10 + if block.off+lastBS > f.length { + lastblock := frange{f.length - lastBS, lastBS} + if f.length < lastBS { + lastblock = frange{0, f.length} + } + f.readAhead(&lastblock) + } + ranges := f.splitRange(block) + reqs := f.prepareRequests(ranges) + defer func() { + for _, req := range reqs { + s := req.s + s.refs-- + if s.refs == 0 && s.state == INVALID { + s.delete() + } + } + }() + f.checkReadahead(block) + return f.waitForIO(ctx, reqs, buf) +} + +func (f *fileReader) visit(fn func(s *sliceReader)) { + var next *sliceReader + for s := f.slices; s != nil; s = next { + next = s.next + fn(s) + } +} + +func (f *fileReader) Close(ctx meta.Context) { + f.Lock() + f.closing = true + f.visit(func(s *sliceReader) { + s.drop() + }) + f.release() + f.Unlock() +} + +type dataReader struct { + sync.Mutex + m meta.Meta + store chunk.ChunkStore + files map[Ino]*fileReader + blockSize uint64 + readAheadMax uint64 + readAheadTotal uint64 + maxRequests int + maxRetries uint32 +} + +func NewDataReader(conf *Config, m meta.Meta, store chunk.ChunkStore) DataReader { + var readAheadTotal = 256 << 20 + var readAheadMax = conf.Chunk.BlockSize * 8 + if conf.Chunk.BufferSize > 0 { + readAheadTotal = conf.Chunk.BufferSize * 8 / 10 // 80% of total buffer + } + if conf.Chunk.Readahead > 0 { + readAheadMax = conf.Chunk.Readahead + } + return &dataReader{ + m: m, + store: store, + files: make(map[Ino]*fileReader), + blockSize: uint64(conf.Chunk.BlockSize), + readAheadTotal: uint64(readAheadTotal), + readAheadMax: uint64(readAheadMax), + maxRequests: readAheadMax/conf.Chunk.BlockSize*readSessions + 1, + maxRetries: uint32(conf.Meta.IORetries), + } +} + +func (r *dataReader) Open(inode Ino, len uint64) FileReader { + f := &fileReader{ + r: r, + inode: inode, + length: len, + } + f.last = &(f.slices) + + r.Lock() + f.refs = 1 + f.next = r.files[inode] + r.files[inode] = f + r.Unlock() + return f +} + +func (r *dataReader) readSlice(ctx context.Context, s *meta.Slice, page *chunk.Page, off int) error { + buf := page.Data + read := 0 + if s.Chunkid == 0 { + for read < len(buf) { + buf[read] = 0 + read++ + } + return nil + } + + reader := r.store.NewReader(s.Chunkid, int(s.Size)) + for read < len(buf) { + p := page.Slice(read, len(buf)-read) + n, err := reader.ReadAt(ctx, p, off+int(s.Off)) + p.Release() + if n == 0 && err != nil { + logger.Warningf("fail to read chunkid %d (off:%d, size:%d, clen: %d): %s", + s.Chunkid, off+int(s.Off), len(buf)-read, s.Size, err) + return err + } + read += n + off += n + } + return nil +} + +func (r *dataReader) Read(ctx context.Context, page *chunk.Page, chunks []meta.Slice, offset uint32) int { + if len(chunks) > 16 { + return r.readManyChunks(ctx, page, chunks, offset) + } + read := 0 + var pos uint32 + errs := make(chan error, 10) + waits := 0 + buf := page.Data + size := len(buf) + for i := 0; i < len(chunks); i++ { + if read < size && offset < pos+chunks[i].Len { + toread := utils.Min(int(size-read), int(pos+chunks[i].Len-offset)) + go func(s *meta.Slice, p *chunk.Page, off, pos uint32) { + defer p.Release() + errs <- r.readSlice(ctx, s, p, int(off)) + }(&chunks[i], page.Slice(read, toread), offset-pos, pos) + read += toread + offset += uint32(toread) + waits++ + } + pos += chunks[i].Len + } + for read < size { + buf[read] = 0 + read++ + } + var err error + // wait for all goroutine to return, otherwise they may access invalid memory + for waits > 0 { + if e := <-errs; e != nil { + err = e + } + waits-- + } + if err != nil { + return 0 + } + return read +} + +func (r *dataReader) readManyChunks(ctx context.Context, page *chunk.Page, chunks []meta.Slice, offset uint32) int { + read := 0 + var pos uint32 + var err error + errs := make(chan error, 10) + waits := 0 + buf := page.Data + size := len(buf) + concurrency := make(chan byte, 16) + +CHUNKS: + for i := 0; i < len(chunks); i++ { + if read < size && offset < pos+chunks[i].Len { + toread := utils.Min(int(size-read), int(pos+chunks[i].Len-offset)) + WAIT: + for { + select { + case concurrency <- 1: + break WAIT + case e := <-errs: + waits-- + if e != nil { + err = e + break CHUNKS + } + } + } + go func(s *meta.Slice, p *chunk.Page, off int, pos uint32) { + defer p.Release() + errs <- r.readSlice(ctx, s, p, off) + <-concurrency + }(&chunks[i], page.Slice(read, toread), int(offset-pos), pos) + + read += toread + offset += uint32(toread) + waits++ + } + pos += chunks[i].Len + } + // wait for all jobs done, otherwise they may access invalid memory + for waits > 0 { + if e := <-errs; e != nil { + err = e + } + waits-- + } + if err != nil { + return 0 + } + for read < size { + buf[read] = 0 + read++ + } + return read +} diff --git a/pkg/vfs/vfs.go b/pkg/vfs/vfs.go new file mode 100644 index 000000000000..2b762490ddf9 --- /dev/null +++ b/pkg/vfs/vfs.go @@ -0,0 +1,899 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package vfs + +import ( + "runtime" + "syscall" + "time" + + "github.com/juicedata/juicefs/pkg/chunk" + "github.com/juicedata/juicefs/pkg/meta" + "github.com/juicedata/juicefs/pkg/utils" +) + +type Ino = meta.Ino +type Attr = meta.Attr +type Context = LogContext + +const ( + rootID = 1 + maxName = 255 + maxSymlink = 4096 + maxFileSize = meta.ChunkSize << 31 + + modeRead = 1 + modeWrite = 2 +) + +type StorageConfig struct { + Name string + Endpoint string + AccessKey string + SecretKey string + Key string + KeyPath string + Passphrase string +} + +type Config struct { + Meta *meta.Config + Format *meta.Format + Primary *StorageConfig + Chunk *chunk.Config + Version string + Mountpoint string + Prefix string +} + +var ( + m meta.Meta + reader DataReader + writer DataWriter +) + +func Lookup(ctx Context, parent Ino, name string) (entry *meta.Entry, err syscall.Errno) { + defer func() { + logit(ctx, "lookup (%d,%s): %s%s", parent, name, strerr(err), (*Entry)(entry)) + }() + nleng := len(name) + if nleng > maxName { + err = syscall.ENAMETOOLONG + return + } + var inode Ino + var attr = &Attr{} + if parent == rootID { + if nleng == 2 && name[0] == '.' && name[1] == '.' { + nleng = 1 + name = name[:1] + } + n := getInternalNodeByName(name) + if n != nil { + entry = &meta.Entry{Inode: n.inode, Attr: n.attr} + return + } + + } + err = m.Lookup(ctx, parent, name, &inode, attr) + if err != 0 { + return + } + if attr.Typ == meta.TypeFile { + maxfleng := writer.GetLength(inode) + if maxfleng > attr.Length { + attr.Length = maxfleng + } + updateHandleLength(inode, attr.Length) + } + entry = &meta.Entry{Inode: inode, Attr: attr} + return +} + +func GetAttr(ctx Context, ino Ino, opened uint8) (entry *meta.Entry, err syscall.Errno) { + defer func() { logit(ctx, "getattr (%d): %s%s", ino, strerr(err), (*Entry)(entry)) }() + if IsSpecialNode(ino) && getInternalNode(ino) != nil { + n := getInternalNode(ino) + entry = &meta.Entry{Inode: n.inode, Attr: n.attr} + return + } + var attr = &Attr{} + err = m.GetAttr(ctx, ino, attr) + if err != 0 { + return + } + if attr.Typ == meta.TypeFile { + maxfleng := writer.GetLength(ino) + if maxfleng > attr.Length { + attr.Length = maxfleng + } + updateHandleLength(ino, attr.Length) + } + entry = &meta.Entry{Inode: ino, Attr: attr} + return +} + +func get_filetype(mode uint16) uint8 { + switch mode & (syscall.S_IFMT & 0xffff) { + case syscall.S_IFIFO: + return meta.TypeFIFO + case syscall.S_IFSOCK: + return meta.TypeSocket + case syscall.S_IFLNK: + return meta.TypeSymlink + case syscall.S_IFREG: + return meta.TypeFile + case syscall.S_IFBLK: + return meta.TypeBlockDev + case syscall.S_IFDIR: + return meta.TypeDirectory + case syscall.S_IFCHR: + return meta.TypeCharDev + } + return meta.TypeFile +} + +func Mknod(ctx Context, parent Ino, name string, mode uint16, cumask uint16, rdev uint32) (entry *meta.Entry, err syscall.Errno) { + nleng := uint8(len(name)) + defer func() { + logit(ctx, "mknod (%d,%s,%s:0%04o,0x%08X): %s%s", parent, name, smode(mode), mode, rdev, strerr(err), (*Entry)(entry)) + }() + if parent == rootID && isSpecialName(name) { + err = syscall.EACCES + return + } + if nleng > maxName { + err = syscall.ENAMETOOLONG + return + } + _type := get_filetype(mode) + if _type == 0 { + err = syscall.EPERM + return + } + + var inode Ino + var attr = &Attr{} + err = m.Mknod(ctx, parent, name, _type, mode&07777, cumask, uint32(rdev), &inode, attr) + if err == 0 { + entry = &meta.Entry{Inode: inode, Attr: attr} + } + return +} + +func Unlink(ctx Context, parent Ino, name string) (err syscall.Errno) { + defer func() { logit(ctx, "unlink (%d,%s): %s", parent, name, strerr(err)) }() + nleng := uint8(len(name)) + if parent == rootID && isSpecialName(name) { + err = syscall.EACCES + return + } + if nleng > maxName { + err = syscall.ENAMETOOLONG + return + } + err = m.Unlink(ctx, parent, name) + return +} + +func Mkdir(ctx Context, parent Ino, name string, mode uint16, cumask uint16) (entry *meta.Entry, err syscall.Errno) { + defer func() { + logit(ctx, "mkdir (%d,%s,%s:0%04o): %s%s", parent, name, smode(mode), mode, strerr(err), (*Entry)(entry)) + }() + nleng := uint8(len(name)) + if parent == rootID && isSpecialName(name) { + err = syscall.EEXIST + return + } + if nleng > maxName { + err = syscall.ENAMETOOLONG + return + } + + var inode Ino + var attr = &Attr{} + err = m.Mkdir(ctx, parent, name, mode, cumask, 0, &inode, attr) + if err == 0 { + entry = &meta.Entry{Inode: inode, Attr: attr} + } + return +} + +func Rmdir(ctx Context, parent Ino, name string) (err syscall.Errno) { + nleng := uint8(len(name)) + defer func() { logit(ctx, "rmdir (%d,%s): %s", parent, name, strerr(err)) }() + if parent == rootID && isSpecialName(name) { + err = syscall.EACCES + return + } + if nleng > maxName { + err = syscall.ENAMETOOLONG + return + } + err = m.Rmdir(ctx, parent, name) + return +} + +func Symlink(ctx Context, path string, parent Ino, name string) (entry *meta.Entry, err syscall.Errno) { + nleng := uint8(len(name)) + defer func() { + logit(ctx, "symlink (%d,%s,%s): %s%s", parent, name, path, strerr(err), (*Entry)(entry)) + }() + if parent == rootID && isSpecialName(name) { + err = syscall.EEXIST + return + } + if nleng > maxName || (len(path)+1) > maxSymlink { + err = syscall.ENAMETOOLONG + return + } + + var inode Ino + var attr = &Attr{} + err = m.Symlink(ctx, parent, name, path, &inode, attr) + if err == 0 { + entry = &meta.Entry{Inode: inode, Attr: attr} + } + return +} + +func Readlink(ctx Context, ino Ino) (path []byte, err syscall.Errno) { + defer func() { logit(ctx, "readlink (%d): %s (%s)", ino, strerr(err), string(path)) }() + err = m.ReadLink(ctx, ino, &path) + return +} + +func Rename(ctx Context, parent Ino, name string, newparent Ino, newname string) (err syscall.Errno) { + defer func() { logit(ctx, "rename (%d,%s,%d,%s): %s", parent, name, newparent, newname, strerr(err)) }() + if parent == rootID && isSpecialName(name) { + err = syscall.EACCES + return + } + if newparent == rootID && isSpecialName(newname) { + err = syscall.EACCES + return + } + if len(name) > maxName || len(newname) > maxName { + err = syscall.ENAMETOOLONG + return + } + + err = m.Rename(ctx, parent, name, newparent, newname, nil, nil) + return +} + +func Link(ctx Context, ino Ino, newparent Ino, newname string) (entry *meta.Entry, err syscall.Errno) { + defer func() { + logit(ctx, "link (%d,%d,%s): %s%s", ino, newparent, newname, strerr(err), (*Entry)(entry)) + }() + if IsSpecialNode(ino) { + err = syscall.EACCES + return + } + if newparent == rootID && isSpecialName(newname) { + err = syscall.EACCES + return + } + if len(newname) > maxName { + err = syscall.ENAMETOOLONG + return + } + + var attr = &Attr{} + err = m.Link(ctx, ino, newparent, newname, attr) + if err == 0 { + entry = &meta.Entry{Inode: ino, Attr: attr} + } + return +} + +func Opendir(ctx Context, ino Ino) (fh uint64, err syscall.Errno) { + defer func() { logit(ctx, "opendir (%d): %s [fh:%d]", ino, strerr(err), fh) }() + if IsSpecialNode(ino) { + err = syscall.ENOTDIR + return + } + fh = newHandle(ino).fh + return +} + +func UpdateEntry(e *meta.Entry) { + attr := e.Attr + if attr.Full && attr.Typ == meta.TypeFile { + maxfleng := writer.GetLength(e.Inode) + if maxfleng > attr.Length { + attr.Length = maxfleng + } + updateHandleLength(e.Inode, attr.Length) + } +} + +func Readdir(ctx Context, ino Ino, size uint32, off int, fh uint64, plus bool) (entries []*meta.Entry, err syscall.Errno) { + defer func() { logit(ctx, "readdir (%d,%d,%d): %s (%d)", ino, size, off, strerr(err), len(entries)) }() + h := findHandle(ino, fh) + if h == nil { + err = syscall.EBADF + return + } + h.Lock() + defer h.Unlock() + + if h.children == nil || off == 0 { + var inodes []*meta.Entry + err = m.Readdir(ctx, ino, 1, &inodes) + if err == syscall.EACCES { + err = m.Readdir(ctx, ino, 0, &inodes) + } + if err != 0 { + return + } + h.children = inodes + if ino == rootID { + // add internal nodes + for _, node := range internalNodes { + h.children = append(h.children, &meta.Entry{ + Inode: node.inode, + Name: []byte(node.name), + Attr: node.attr, + }) + } + } + } + if off < len(h.children) { + entries = h.children[off:] + } + return +} + +func Releasedir(ctx Context, ino Ino, fh uint64) int { + h := findHandle(ino, fh) + if h == nil { + return 0 + } + ReleaseHandler(ino, fh) + logit(ctx, "releasedir (%d): OK", ino) + return 0 +} + +func Create(ctx Context, parent Ino, name string, mode uint16, cumask uint16, flags uint32) (entry *meta.Entry, fh uint64, err syscall.Errno) { + defer func() { + logit(ctx, "create (%d,%s,%s:0%04o): %s%s [fh:%d]", parent, name, smode(mode), mode, strerr(err), (*Entry)(entry), fh) + }() + if parent == rootID && isSpecialName(name) { + err = syscall.EEXIST + return + } + if len(name) > maxName { + err = syscall.ENAMETOOLONG + return + } + + var inode Ino + var attr = &Attr{} + var oflags uint8 + switch flags & O_ACCMODE { + case syscall.O_RDONLY: + oflags |= modeRead + case syscall.O_WRONLY: + oflags |= modeWrite + case syscall.O_RDWR: + oflags |= modeRead | modeWrite + } + err = m.Create(ctx, parent, name, mode&07777, cumask, &inode, attr) + if runtime.GOOS == "darwin" && err == syscall.ENOENT { + err = syscall.EACCES + } + if err != 0 { + return + } + + fh = newFileHandle(oflags, inode, 0) + entry = &meta.Entry{Inode: inode, Attr: attr} + return +} + +func Open(ctx Context, ino Ino, flags uint32) (entry *meta.Entry, fh uint64, err syscall.Errno) { + var attr = &Attr{} + defer func() { + if entry != nil { + logit(ctx, "open (%d): %s [fh:%d]", ino, strerr(err), fh) + } else { + logit(ctx, "open (%d): %s", ino, strerr(err)) + } + }() + if IsSpecialNode(ino) { + if (flags & O_ACCMODE) != syscall.O_RDONLY { + err = syscall.EACCES + return + } + h := newHandle(ino) + fh = h.fh + switch ino { + case logInode: + openAccessLog(fh) + } + n := getInternalNode(ino) + entry = &meta.Entry{Inode: ino, Attr: n.attr} + return + } + + var oflags uint8 + switch flags & O_ACCMODE { + case syscall.O_RDONLY: + oflags |= modeRead + case syscall.O_WRONLY: + oflags |= modeWrite + case syscall.O_RDWR: + oflags |= modeRead | modeWrite + default: + err = syscall.EINVAL + return + } + err = m.Open(ctx, ino, oflags, attr) + if err != 0 { + return + } + + maxfleng := writer.GetLength(ino) + if maxfleng > attr.Length { + attr.Length = maxfleng + } + fh = newFileHandle(oflags, ino, attr.Length) + entry = &meta.Entry{Inode: ino, Attr: attr} + return +} + +func Truncate(ctx Context, ino Ino, size int64, opened uint8, attr *Attr) (err syscall.Errno) { + defer func() { logit(ctx, "truncate (%d,%d): %s", ino, size, strerr(err)) }() + if IsSpecialNode(ino) { + err = syscall.EPERM + return + } + if size < 0 { + err = syscall.EINVAL + return + } + if size >= maxFileSize { + err = syscall.EFBIG + return + } + writer.Flush(ctx, ino) + var flags uint8 + trycnt := 0 + if attr == nil { + attr = &Attr{} + } + for { + err = m.Truncate(ctx, ino, flags, uint64(size), attr) + if err == syscall.ENOTSUP { + err = m.GetAttr(ctx, ino, attr) + if err == 0 { + fleng := attr.Length + logger.Debugf("fill zero %d-%d", fleng, size) + w := writer.Open(ino, fleng) + block := make([]byte, 1<<17) + for fleng < uint64(size) { + n := uint64(len(block)) + if fleng+n > uint64(size) { + n = uint64(size) - fleng + } + err = w.Write(ctx, fleng, block[:n]) + if err != 0 { + err = syscall.EIO + break + } + fleng += n + } + if fleng == uint64(size) { + err = w.Close(ctx) + } else { + w.Close(ctx) + } + } + if err != 0 { + err = syscall.EINTR // retry + } + } + if err == 0 || err == syscall.EROFS || err == syscall.EACCES || err == syscall.EPERM || err == syscall.ENOENT || err == syscall.ENOSPC || err == syscall.EINTR { + break + } else { + trycnt++ + if trycnt >= 30 { + break + } + t := 1 + (trycnt-1)*300 + if trycnt > 30 { + t = 10000 + } + time.Sleep(time.Millisecond * time.Duration(t)) + } + } + if err != 0 { + return + } + updateHandleLength(ino, uint64(size)) + writer.Truncate(ino, uint64(size)) + return 0 +} + +func ReleaseHandler(ino Ino, fh uint64) { + releaseFileHandle(ino, fh) +} + +func Release(ctx Context, ino Ino, fh uint64) (err syscall.Errno) { + defer func() { logit(ctx, "release (%d): %s", ino, strerr(err)) }() + if IsSpecialNode(ino) { + if ino == logInode { + closeAccessLog(fh) + } + releaseHandle(ino, fh) + return + } + if fh > 0 { + f := findHandle(ino, fh) + if f != nil { + f.Lock() + // rwlock_wait_for_unlock: + for (f.writing | f.writers | f.readers) != 0 { + if f.cond.WaitWithTimeout(time.Millisecond*100) && ctx.Canceled() { + f.Unlock() + err = syscall.EINTR + return + } + } + locks := f.locks + owner := f.flockOwner + f.Unlock() + if f.writer != nil { + f.writer.Close(ctx) + } + if locks&1 != 0 { + m.Flock(ctx, ino, owner, syscall.F_UNLCK, false) + } + } + m.Close(ctx, ino) + go releaseFileHandle(ino, fh) // after writes it waits for data sync, so do it after everything + } + return +} + +func Read(ctx Context, ino Ino, buf []byte, off uint64, fh uint64) (n int, err syscall.Errno) { + size := uint32(len(buf)) + if IsSpecialNode(ino) { + if ino == logInode { + n = readAccessLog(fh, buf) + } else { + h := findHandle(ino, fh) + if h == nil { + err = syscall.EBADF + return + } + data := h.data + if int(off) < len(data) { + data = data[off:] + if int(size) < len(data) { + data = data[:size] + } + n = copy(buf, data) + } + logit(ctx, "read (%d,%d,%d): OK (%d)", ino, size, off, n) + } + return + } + + defer func() { + logit(ctx, "read (%d,%d,%d): %s (%d)", ino, size, off, strerr(err), n) + }() + h := findHandle(ino, fh) + if h == nil { + err = syscall.EBADF + return + } + if off >= maxFileSize || off+uint64(size) >= maxFileSize { + err = syscall.EFBIG + return + } + if h.mode == modeWrite { + err = syscall.EACCES + return + } + if !h.Rlock(ctx) { + err = syscall.EINTR + return + } + defer h.Runlock() + + writer.Flush(ctx, ino) + n, err = h.reader.Read(ctx, off, buf) + if err == syscall.ENOENT { + err = syscall.EBADF + } + h.removeOp(ctx) + return +} + +func Write(ctx Context, ino Ino, buf []byte, off, fh uint64) (err syscall.Errno) { + size := uint64(len(buf)) + defer func() { logit(ctx, "write (%d,%d,%d): %s", ino, size, off, strerr(err)) }() + if IsSpecialNode(ino) { + err = syscall.EACCES + return + } + h := findHandle(ino, fh) + if h == nil { + err = syscall.EBADF + return + } + if off >= maxFileSize || off+size >= maxFileSize { + err = syscall.EFBIG + return + } + if h.mode == modeRead { + err = syscall.EACCES + return + } + + if !h.Wlock(ctx) { + err = syscall.EINTR + return + } + defer h.Wunlock() + + err = h.writer.Write(ctx, off, buf) + if err == syscall.ENOENT || err == syscall.EPERM || err == syscall.EINVAL { + err = syscall.EBADF + } + h.removeOp(ctx) + + if err != 0 { + return + } + + h.Lock() + var newfleng uint64 + if off+size > h.length { + newfleng = off + size + h.length = newfleng + } + h.Unlock() + if newfleng > 0 { + writer.Truncate(ino, newfleng) + updateHandleLength(ino, newfleng) + } + return +} + +func Fallocate(ctx Context, ino Ino, mode uint8, off, length int64, fh uint64) (err syscall.Errno) { + defer func() { logit(ctx, "fallocate (%d,%d,%d,%d): %s", ino, mode, off, length, strerr(err)) }() + if off < 0 || length <= 0 { + err = syscall.EINVAL + return + } + if IsSpecialNode(ino) { + err = syscall.EACCES + return + } + h := findHandle(ino, fh) + if h == nil { + err = syscall.EBADF + return + } + if off >= maxFileSize || off+length >= maxFileSize { + err = syscall.EFBIG + return + } + if h.mode == modeRead { + err = syscall.EACCES + return + } + if !h.Wlock(ctx) { + err = syscall.EINTR + return + } + defer h.Wunlock() + defer h.removeOp(ctx) + + err = m.Fallocate(ctx, ino, mode, uint64(off), uint64(length)) + return +} + +func doFsync(ctx Context, h *handle) (err syscall.Errno) { + h.Lock() + if h.writer != nil && (h.mode != modeRead) { + h.Unlock() + if !h.Wlock(ctx) { + return syscall.EINTR + } + defer h.Wunlock() + defer h.removeOp(ctx) + + err = h.writer.Flush(ctx) + if err == syscall.ENOENT || err == syscall.EPERM || err == syscall.EINVAL { + err = syscall.EBADF + } + } else { + h.Unlock() + } + return err +} + +func Flush(ctx Context, ino Ino, fh uint64, lockOwner uint64) (err syscall.Errno) { + defer func() { logit(ctx, "flush (%d): %s", ino, strerr(err)) }() + if IsSpecialNode(ino) { + return + } + h := findHandle(ino, fh) + if h == nil { + err = syscall.EBADF + return + } + + h.Lock() + locks := h.locks + if h.writer != nil && (h.mode != modeRead) { + h.Unlock() + if !h.Wlock(ctx) { + h.cancelOp(ctx.Pid()) + err = syscall.EINTR + return + } + + err = h.writer.Flush(ctx) + if err == syscall.ENOENT || err == syscall.EPERM || err == syscall.EINVAL { + err = syscall.EBADF + } + h.removeOp(ctx) + h.Wunlock() + h.Lock() + } else if h.reader != nil { + h.cancelOp(ctx.Pid()) + } + h.Unlock() + if locks&2 != 0 { + m.Setlk(ctx, ino, lockOwner, false, syscall.F_UNLCK, 0, 0x7FFFFFFFFFFFFFFF, 0) + } + return +} + +func Fsync(ctx Context, ino Ino, datasync int, fh uint64) (err syscall.Errno) { + defer func() { logit(ctx, "fsync (%d,%d): %s", ino, datasync, strerr(err)) }() + if IsSpecialNode(ino) { + return + } + h := findHandle(ino, fh) + if h == nil { + err = syscall.EBADF + return + } + err = doFsync(ctx, h) + return +} + +const ( + xattrMaxName = 255 + xattrMaxSize = 65536 +) + +func SetXattr(ctx Context, ino Ino, name string, value []byte, flags int) (err syscall.Errno) { + defer func() { logit(ctx, "setxattr (%d,%s,%d,%d): %s", ino, name, len(value), flags, strerr(err)) }() + if IsSpecialNode(ino) { + err = syscall.EPERM + return + } + if len(value) > xattrMaxSize { + if runtime.GOOS == "darwin" { + err = syscall.E2BIG + } else { + err = syscall.ERANGE + } + return + } + if len(name) > xattrMaxName { + if runtime.GOOS == "darwin" { + err = syscall.EPERM + } else { + err = syscall.ERANGE + } + return + } + if len(name) == 0 { + err = syscall.EINVAL + return + } + if name == "system.posix_acl_access" || name == "system.posix_acl_default" { + err = syscall.ENOTSUP + return + } + err = m.SetXattr(ctx, ino, name, value) + return +} + +func GetXattr(ctx Context, ino Ino, name string, size uint32) (value []byte, err syscall.Errno) { + defer func() { logit(ctx, "getxattr (%d,%s,%d): %s (%d)", ino, name, size, strerr(err), len(value)) }() + + if IsSpecialNode(ino) { + err = syscall.EPERM + return + } + if len(name) > xattrMaxName { + if runtime.GOOS == "darwin" { + err = syscall.EPERM + } else { + err = syscall.ERANGE + } + return + } + if len(name) == 0 { + err = syscall.EINVAL + return + } + if name == "system.posix_acl_access" || name == "system.posix_acl_default" { + err = syscall.ENOTSUP + return + } + err = m.GetXattr(ctx, ino, name, &value) + if size > 0 && len(value) > int(size) { + err = syscall.ERANGE + } + return +} + +func ListXattr(ctx Context, ino Ino, size int) (data []byte, err syscall.Errno) { + defer func() { logit(ctx, "listxattr (%d,%d): %s (%d)", ino, size, strerr(err), len(data)) }() + if IsSpecialNode(ino) { + err = syscall.EPERM + return + } + err = m.ListXattr(ctx, ino, &data) + if size > 0 && len(data) > size { + err = syscall.ERANGE + } + return +} + +func RemoveXattr(ctx Context, ino Ino, name string) (err syscall.Errno) { + defer func() { logit(ctx, "removexattr (%d,%s): %s", ino, name, strerr(err)) }() + if IsSpecialNode(ino) { + err = syscall.EPERM + return + } + if name == "system.posix_acl_access" || name == "system.posix_acl_default" { + return syscall.ENOTSUP + } + if len(name) > xattrMaxName { + if runtime.GOOS == "darwin" { + err = syscall.EPERM + } else { + err = syscall.ERANGE + } + return + } + if len(name) == 0 { + err = syscall.EINVAL + return + } + err = m.RemoveXattr(ctx, ino, name) + return +} + +var logger = utils.GetLogger("juicefs") + +func Init(conf *Config, m_ meta.Meta, store chunk.ChunkStore) { + m = m_ + reader = NewDataReader(conf, m, store) + writer = NewDataWriter(conf, m, store) + handles = make(map[Ino][]*handle) +} diff --git a/pkg/vfs/vfs_unix.go b/pkg/vfs/vfs_unix.go new file mode 100644 index 000000000000..aeaab72a33f7 --- /dev/null +++ b/pkg/vfs/vfs_unix.go @@ -0,0 +1,281 @@ +// +build !windows + +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package vfs + +import ( + "fmt" + "syscall" + + "github.com/juicedata/juicefs/pkg/meta" + + "golang.org/x/sys/unix" +) + +const O_ACCMODE = syscall.O_ACCMODE + +const ( + MODE_MASK_R = 4 + MODE_MASK_W = 2 + MODE_MASK_X = 1 +) + +type Statfs struct { + Bsize uint32 + Blocks uint64 + Bavail uint64 + Files uint64 + Favail uint64 +} + +func StatFS(ctx Context, ino Ino) (st *Statfs, err int) { + var totalspace, availspace, iused, iavail uint64 + m.StatFS(ctx, &totalspace, &availspace, &iused, &iavail) + var bsize uint64 = 0x10000 + blocks := totalspace / bsize + bavail := blocks - (totalspace-availspace+bsize-1)/bsize + + st = new(Statfs) + st.Bsize = uint32(bsize) + st.Blocks = blocks + st.Bavail = bavail + st.Files = iused + iavail + st.Favail = iavail + logit(ctx, "statfs (%d): OK (%d,%d,%d,%d)", ino, totalspace-availspace, availspace, iused, iavail) + return +} + +func accessTest(attr *Attr, mmode uint16, uid uint32, gid uint32) syscall.Errno { + if uid == 0 { + return 0 + } + mode := attr.Mode + var effected uint16 + if uid == attr.Uid { + effected = (mode >> 6) & 7 + } else { + effected = mode & 7 + if gid == attr.Gid { + effected = (mode >> 3) & 7 + } + } + if mmode&effected != mmode { + return syscall.EACCES + } + return 0 +} + +func Access(ctx Context, ino Ino, mask int) (err syscall.Errno) { + defer func() { logit(ctx, "access (%d,0x%X): %s", ino, mask, strerr(err)) }() + var mmode uint16 + if mask&unix.R_OK != 0 { + mmode |= MODE_MASK_R + } + if mask&unix.W_OK != 0 { + mmode |= MODE_MASK_W + } + if mask&unix.X_OK != 0 { + mmode |= MODE_MASK_X + } + if IsSpecialNode(ino) { + node := getInternalNode(ino) + err = accessTest(node.attr, mmode, ctx.Uid(), ctx.Gid()) + return + } + + err = m.Access(ctx, ino, mmode) + return +} + +func setattrStr(set int, mode, uid, gid uint32, atime, mtime int64, size uint64) string { + s := "" + if set&meta.SetAttrMode != 0 { + s += fmt.Sprintf("mode=%s:0%04o;", smode(uint16(mode)), (mode & 07777)) + } + if set&meta.SetAttrUID != 0 { + s += fmt.Sprintf("uid=%d;", uid) + } + if set&meta.SetAttrGID != 0 { + s += fmt.Sprintf("gid=%d;", gid) + } + if (set&meta.SetAttrAtime) != 0 && atime < 0 { + s += fmt.Sprintf("atime=NOW;") + } else if set&meta.SetAttrAtime != 0 { + s += fmt.Sprintf("atime=%d;", atime) + } + if (set&meta.SetAttrMtime) != 0 && mtime < 0 { + s += fmt.Sprintf("mtime=NOW;") + } else if set&meta.SetAttrMtime != 0 { + s += fmt.Sprintf("mtime=%d;", mtime) + } + if (set & meta.SetAttrSize) != 0 { + s += fmt.Sprintf("size=%d;", size) + } + return s +} + +func SetAttr(ctx Context, ino Ino, set int, opened uint8, mode, uid, gid uint32, atime, mtime int64, atimensec, mtimensec uint32, size uint64) (entry *meta.Entry, err syscall.Errno) { + str := setattrStr(set, mode, uid, gid, atime, mtime, size) + defer func() { + logit(ctx, "setattr (%d,0x%X,[%s]): %s%s", ino, set, str, strerr(err), (*Entry)(entry)) + }() + if IsSpecialNode(ino) { + n := getInternalNode(ino) + entry = &meta.Entry{Inode: ino, Attr: n.attr} + return + } + err = syscall.EINVAL + var attr = &Attr{} + if (set & (meta.SetAttrMode | meta.SetAttrUID | meta.SetAttrGID | meta.SetAttrAtime | meta.SetAttrMtime | meta.SetAttrSize)) == 0 { + // change other flags or change nothing + err = m.SetAttr(ctx, ino, 0, 0, attr) + if err != 0 { + return + } + } + if (set & (meta.SetAttrMode | meta.SetAttrUID | meta.SetAttrGID | meta.SetAttrAtime | meta.SetAttrMtime | meta.SetAttrAtimeNow | meta.SetAttrMtimeNow)) != 0 { + if (set & meta.SetAttrMode) != 0 { + attr.Mode = uint16(mode & 07777) + } + if (set & meta.SetAttrUID) != 0 { + attr.Uid = uid + } + if (set & meta.SetAttrGID) != 0 { + attr.Gid = gid + } + if set&meta.SetAttrAtime != 0 { + attr.Atime = atime + attr.Atimensec = atimensec + } + if (set & meta.SetAttrMtime) != 0 { + attr.Mtime = mtime + attr.Mtimensec = mtimensec + } + err = m.SetAttr(ctx, ino, uint16(set), 0, attr) + if err != 0 { + return + } + } + if set&meta.SetAttrSize != 0 { + err = Truncate(ctx, ino, int64(size), opened, attr) + } + entry = &meta.Entry{Inode: ino, Attr: attr} + return +} + +type lockType uint32 + +func (l lockType) String() string { + switch l { + case syscall.F_UNLCK: + return "U" + case syscall.F_RDLCK: + return "R" + case syscall.F_WRLCK: + return "W" + default: + return "X" + } +} + +func Getlk(ctx Context, ino Ino, fh uint64, owner uint64, start, len *uint64, typ *uint32, pid *uint32) (err syscall.Errno) { + logit(ctx, "getlk (%d,%016X): %s (%d,%d,%s,%d)", ino, owner, strerr(err), *start, *len, lockType(*typ), *pid) + if lockType(*typ).String() == "X" { + return syscall.EINVAL + } + if IsSpecialNode(ino) { + err = syscall.EPERM + return + } + if findHandle(ino, fh) == nil { + err = syscall.EBADF + return + } + err = m.Getlk(ctx, ino, owner, typ, start, len, pid) + return +} + +func Setlk(ctx Context, ino Ino, fh uint64, owner uint64, start, end uint64, typ uint32, pid uint32, block bool) (err syscall.Errno) { + defer func() { + logit(ctx, "setlk (%d,%016X,%d,%d,%s,%t,%d): %s", ino, owner, start, end, lockType(typ), block, pid, strerr(err)) + }() + if lockType(typ).String() == "X" { + return syscall.EINVAL + } + if IsSpecialNode(ino) { + err = syscall.EPERM + return + } + h := findHandle(ino, fh) + if h == nil { + err = syscall.EBADF + return + } + h.addOp(ctx) + defer h.removeOp(ctx) + + err = m.Setlk(ctx, ino, owner, block, typ, start, end, pid) + if err == 0 { + h.Lock() + if typ != syscall.F_UNLCK { + h.locks |= 2 + } + h.Unlock() + } + return +} + +func Flock(ctx Context, ino Ino, fh uint64, owner uint64, typ uint32, block bool) (err syscall.Errno) { + var name string + var reqid uint32 + defer func() { logit(ctx, "flock (%d,%d,%016X,%s,%t): %s", reqid, ino, owner, name, block, strerr(err)) }() + switch typ { + case syscall.F_RDLCK: + name = "LOCKSH" + case syscall.F_WRLCK: + name = "LOCKEX" + case syscall.F_UNLCK: + name = "UNLOCK" + default: + err = syscall.EINVAL + return + } + + if IsSpecialNode(ino) { + err = syscall.EPERM + return + } + h := findHandle(ino, fh) + if h == nil { + err = syscall.EBADF + return + } + h.addOp(ctx) + defer h.removeOp(ctx) + err = m.Flock(ctx, ino, owner, typ, block) + if err == 0 { + h.Lock() + if typ == syscall.F_UNLCK { + h.locks &= 2 + } else { + h.locks |= 1 + h.flockOwner = owner + } + h.Unlock() + } + return +} diff --git a/pkg/vfs/writer.go b/pkg/vfs/writer.go new file mode 100644 index 000000000000..94c8405e8ebd --- /dev/null +++ b/pkg/vfs/writer.go @@ -0,0 +1,485 @@ +/* + * JuiceFS, Copyright (C) 2020 Juicedata, Inc. + * + * This program is free software: you can use, redistribute, and/or modify + * it under the terms of the GNU Affero General Public License, version 3 + * or later ("AGPL"), as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package vfs + +import ( + "runtime" + "sync" + "syscall" + "time" + + "github.com/juicedata/juicefs/pkg/chunk" + "github.com/juicedata/juicefs/pkg/meta" + "github.com/juicedata/juicefs/pkg/utils" +) + +const ( + flushDuration = time.Second * 5 +) + +type FileWriter interface { + Write(ctx meta.Context, offset uint64, data []byte) syscall.Errno + Flush(ctx meta.Context) syscall.Errno + Close(ctx meta.Context) syscall.Errno + GetLength() uint64 + Truncate(maxfleng uint64) +} + +type DataWriter interface { + Open(inode Ino, fleng uint64) FileWriter + Flush(ctx meta.Context, inode Ino) syscall.Errno + GetLength(inode Ino) uint64 + Truncate(inode Ino, maxfleng uint64) +} + +type sliceWriter struct { + id uint64 + chunk *chunkWriter + off uint32 + length uint32 + soff uint32 + slen uint32 + writer chunk.Writer + freezed bool + done bool + err syscall.Errno + notify *utils.Cond + started time.Time + lastMod time.Time +} + +func (s *sliceWriter) prepareID(ctx meta.Context, retry bool) { + f := s.chunk.file + f.Lock() + for s.id == 0 { + var id uint64 + f.Unlock() + st := f.w.m.NewChunk(ctx, f.inode, s.chunk.indx, s.off, &id) + f.Lock() + if st != 0 && st != syscall.EIO { + s.err = st + break + } + if !retry || st == 0 { + if s.id == 0 { + s.id = id + } + break + } + f.Unlock() + logger.Debugf("meta is not availble: %s", st) + time.Sleep(time.Millisecond * 100) + f.Lock() + } + if s.writer != nil && s.writer.ID() == 0 { + s.writer.SetID(s.id) + } + f.Unlock() +} + +func (s *sliceWriter) markDone() { + f := s.chunk.file + f.Lock() + s.done = true + s.notify.Signal() + f.Unlock() +} + +// freezed, no more data +func (s *sliceWriter) flushData() { + defer s.markDone() + if s.slen == 0 { + return + } + s.prepareID(meta.Background, true) + if s.err != 0 { + logger.Infof("flush inode:%d chunk: %s", s.chunk.file.inode, s.err) + return + } + s.length = s.slen + if err := s.writer.Finish(int(s.length)); err != nil { + logger.Errorf("upload chunk %v (length: %v) fail: %s", s.id, s.length, err) + s.writer.Abort() + s.err = syscall.EIO + } + s.writer = nil +} + +// protected by s.chunk.file +func (s *sliceWriter) write(ctx meta.Context, off uint32, data []uint8) syscall.Errno { + f := s.chunk.file + _, err := s.writer.WriteAt(data, int64(off)) + if err != nil { + logger.Warnf("write: chunk: %d off: %d %s", s.id, off, err) + return syscall.EIO + } + if off+uint32(len(data)) > s.slen { + s.slen = off + uint32(len(data)) + } + s.lastMod = time.Now() + if s.slen == meta.ChunkSize { + s.freezed = true + go s.flushData() + } else if int(s.slen) >= f.w.blockSize { + if s.id > 0 { + s.writer.FlushTo(int(s.slen)) + } else if int(off) <= f.w.blockSize { + go s.prepareID(ctx, false) + } + } + return 0 +} + +type chunkWriter struct { + indx uint32 + file *fileWriter + slices []*sliceWriter +} + +// protected by file +func (c *chunkWriter) findWritableSlice(pos uint32, size uint32) *sliceWriter { + blockSize := uint32(c.file.w.blockSize) + for i := range c.slices { + s := c.slices[len(c.slices)-1-i] + if !s.freezed { + flushoff := s.slen / blockSize * blockSize + if pos >= s.off+flushoff && pos <= s.off+s.slen { + return s + } else if i > 3 { + s.freezed = true + go s.flushData() + } + } + if pos < s.off+s.slen && s.off < pos+size { + // overlaped + // TODO: write into multiple slices + return nil + } + } + return nil +} + +func (c *chunkWriter) commitThread() { + f := c.file + defer f.w.free(f) + f.Lock() + defer f.Unlock() + // the slices should be committed in the order that are created + for len(c.slices) > 0 { + s := c.slices[0] + for !s.done { + if s.notify.WaitWithTimeout(time.Millisecond*100) && !s.freezed && time.Since(s.started) > flushDuration*2 { + s.freezed = true + go s.flushData() + } + } + err := s.err + f.Unlock() + + if err == 0 { + var ss = meta.Slice{s.id, s.length, s.soff, s.slen} + err = f.w.m.Write(meta.Background, f.inode, c.indx, s.off, ss) + } + + f.Lock() + if err != 0 { + if err != syscall.ENOENT && err != syscall.ENOSPC { + logger.Warnf("write inode:%d error: %s", f.inode, err) + err = syscall.EIO + } + f.err = err + logger.Errorf("write inode:%d indx:%d %s", f.inode, c.indx, err) + } + c.slices = c.slices[1:] + } + f.freeChunk(c) +} + +type fileWriter struct { + sync.Mutex + w *dataWriter + + inode Ino + length uint64 + err syscall.Errno + flushwaiting uint16 + writewaiting uint16 + refs uint16 + chunks map[uint32]*chunkWriter + + flushcond *utils.Cond // wait for chunks==nil (flush) + writecond *utils.Cond // wait for flushwaiting==0 (write) +} + +// protected by file +func (f *fileWriter) findChunk(i uint32) *chunkWriter { + c := f.chunks[i] + if c == nil { + c = &chunkWriter{indx: i, file: f} + f.chunks[i] = c + } + return c +} + +// protected by file +func (f *fileWriter) freeChunk(c *chunkWriter) { + delete(f.chunks, c.indx) + if len(f.chunks) == 0 && f.flushwaiting > 0 { + f.flushcond.Broadcast() + } +} + +// protected by file +func (f *fileWriter) writeChunk(ctx meta.Context, indx uint32, off uint32, data []byte) syscall.Errno { + c := f.findChunk(indx) + s := c.findWritableSlice(off, uint32(len(data))) + if s == nil { + s = &sliceWriter{ + chunk: c, + off: off, + writer: f.w.store.NewWriter(0), + notify: utils.NewCond(&f.Mutex), + started: time.Now(), + } + c.slices = append(c.slices, s) + if len(c.slices) == 1 { + f.w.Lock() + f.refs++ + f.w.Unlock() + go c.commitThread() + } + } + return s.write(ctx, off-s.off, data) +} + +func (f *fileWriter) Write(ctx meta.Context, off uint64, data []byte) syscall.Errno { + if utils.UsedMemory() > f.w.bufferSize { + // slow down + time.Sleep(time.Millisecond * 10) + } + f.Lock() + defer f.Unlock() + size := uint64(len(data)) + f.writewaiting++ + for f.flushwaiting > 0 { + if f.writecond.WaitWithTimeout(time.Millisecond*100) && ctx.Canceled() { + f.writewaiting-- + return syscall.EINTR + } + } + f.writewaiting-- + + indx := uint32(off / meta.ChunkSize) + pos := uint32(off % meta.ChunkSize) + for len(data) > 0 { + n := uint32(len(data)) + if pos+n > meta.ChunkSize { + n = meta.ChunkSize - pos + } + if st := f.writeChunk(ctx, indx, pos, data[:n]); st != 0 { + return st + } + data = data[n:] + indx++ + pos = (pos + n) % meta.ChunkSize + } + if off+size > f.length { + f.length = off + size + } + return f.err +} + +func (f *fileWriter) flush(ctx meta.Context, writeback bool) syscall.Errno { + f.Lock() + defer f.Unlock() + f.flushwaiting++ + + var err syscall.Errno + var wait = time.Second * time.Duration((f.w.maxRetries+1)*(f.w.maxRetries+1)/2) + if wait < time.Minute*5 { + wait = time.Minute * 5 + } + var deadline = time.Now().Add(wait) + for len(f.chunks) > 0 && err == 0 { + for _, c := range f.chunks { + for _, s := range c.slices { + if !s.freezed { + s.freezed = true + go s.flushData() + } + } + } + if f.flushcond.WaitWithTimeout(time.Second) && ctx.Canceled() { + err = syscall.EINTR + break + } + if time.Now().After(deadline) { + logger.Errorf("flush %d timeout after waited %s", f.inode, wait) + for _, c := range f.chunks { + for _, s := range c.slices { + logger.Errorf("pending slice %d-%d: %+v", f.inode, c.indx, *s) + } + } + buf := make([]byte, 1<<20) + n := runtime.Stack(buf, true) + logger.Warnf("All goroutines (%d):\n%s", runtime.NumGoroutine(), buf[:n]) + err = syscall.EIO + break + } + } + f.flushwaiting-- + if f.flushwaiting == 0 && f.writewaiting > 0 { + f.writecond.Broadcast() + } + if err == 0 { + err = f.err + } + return err +} + +func (f *fileWriter) Flush(ctx meta.Context) syscall.Errno { + return f.flush(ctx, false) +} + +func (f *fileWriter) Close(ctx meta.Context) syscall.Errno { + defer func() { + f.Lock() + f.Unlock() + f.w.free(f) + }() + return f.Flush(ctx) +} + +func (f *fileWriter) GetLength() uint64 { + f.Lock() + defer f.Unlock() + return f.length +} + +func (f *fileWriter) Truncate(length uint64) { + f.Lock() + defer f.Unlock() + if length < f.length { + // TODO: truncate write buffer + } + f.length = length +} + +type dataWriter struct { + sync.Mutex + m meta.Meta + store chunk.ChunkStore + blockSize int + bufferSize int64 + files map[Ino]*fileWriter + maxRetries uint32 +} + +func NewDataWriter(conf *Config, m meta.Meta, store chunk.ChunkStore) DataWriter { + w := &dataWriter{ + m: m, + store: store, + blockSize: conf.Chunk.BlockSize, + bufferSize: int64(conf.Chunk.BufferSize), + files: make(map[Ino]*fileWriter), + maxRetries: uint32(conf.Meta.IORetries), + } + go w.flushAll() + return w +} + +func (w *dataWriter) flushAll() { + for { + w.Lock() + now := time.Now() + for _, f := range w.files { + f.refs++ + w.Unlock() + f.Lock() + + for _, c := range f.chunks { + for _, s := range c.slices { + if !s.freezed && (now.Sub(s.started) > flushDuration || now.Sub(s.lastMod) > time.Second) { + s.freezed = true + go s.flushData() + } + } + } + f.Unlock() + w.free(f) + w.Lock() + } + w.Unlock() + time.Sleep(time.Second) + } +} + +func (w *dataWriter) Open(inode Ino, len uint64) FileWriter { + w.Lock() + defer w.Unlock() + f, ok := w.files[inode] + if !ok { + f = &fileWriter{ + w: w, + inode: inode, + length: len, + chunks: make(map[uint32]*chunkWriter), + } + f.flushcond = utils.NewCond(f) + f.writecond = utils.NewCond(f) + w.files[inode] = f + } + f.refs++ + return f +} + +func (w *dataWriter) find(inode Ino) *fileWriter { + w.Lock() + defer w.Unlock() + return w.files[inode] +} + +func (w *dataWriter) free(f *fileWriter) { + w.Lock() + defer w.Unlock() + f.refs-- + if f.refs == 0 { + delete(w.files, f.inode) + } +} + +func (w *dataWriter) Flush(ctx meta.Context, inode Ino) syscall.Errno { + f := w.find(inode) + if f != nil { + return f.Flush(ctx) + } + return 0 +} + +func (w *dataWriter) GetLength(inode Ino) uint64 { + f := w.find(inode) + if f != nil { + return f.GetLength() + } + return 0 +} + +func (w *dataWriter) Truncate(inode Ino, len uint64) { + f := w.find(inode) + if f != nil { + f.Truncate(len) + } +}