├── .editorconfig ├── .github └── workflows │ ├── pre-commit.yml │ └── tests.yml ├── .gitignore ├── .meta.toml ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGES.rst ├── CONTRIBUTING.md ├── COPYING ├── COPYRIGHT.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── buildout.cfg ├── docs ├── blob-nfs.rst ├── changelog.rst ├── client-cache-tracing.rst ├── client-cache.rst ├── clients.rst ├── conf.py ├── index.rst ├── introduction.rst ├── nagios.rst ├── ordering.rst ├── protocol.rst ├── reference.rst ├── requirements.txt └── server.rst ├── log.ini ├── perf.py ├── pyproject.toml ├── setup.cfg ├── setup.py ├── src └── ZEO │ ├── ClientStorage.py │ ├── Exceptions.py │ ├── StorageServer.py │ ├── TransactionBuffer.py │ ├── __init__.py │ ├── _compat.py │ ├── _forker.py │ ├── asyncio │ ├── README.rst │ ├── __init__.py │ ├── _futures.pyx │ ├── _smp.pyx │ ├── base.py │ ├── client.py │ ├── compat.py │ ├── futures.py │ ├── marshal.py │ ├── server.py │ ├── smp.py │ ├── testing.py │ └── tests.py │ ├── cache.py │ ├── component.xml │ ├── interfaces.py │ ├── monitor.py │ ├── nagios.py │ ├── nagios.rst │ ├── runzeo.py │ ├── schema.xml │ ├── scripts │ ├── README.txt │ ├── __init__.py │ ├── cache_simul.py │ ├── cache_stats.py │ ├── parsezeolog.py │ ├── tests.py │ ├── timeout.py │ ├── zeopack.py │ ├── zeopack.test │ ├── zeoqueue.py │ ├── zeoreplay.py │ ├── zeoserverlog.py │ └── zeoup.py │ ├── server.xml │ ├── shortrepr.py │ ├── tests │ ├── Cache.py │ ├── CommitLockTests.py │ ├── ConnectionTests.py │ ├── InvalidationTests.py │ ├── IterationTests.py │ ├── TestThread.py │ ├── ThreadTests.py │ ├── __init__.py │ ├── client-config.test │ ├── client.pem │ ├── client_key.pem │ ├── component.xml │ ├── drop_cache_rather_than_verify.txt │ ├── dynamic_server_ports.test │ ├── forker.py │ ├── invalidation-age.txt │ ├── new_addr.test │ ├── protocols.test │ ├── racetest.py │ ├── server.pem │ ├── server.pem.csr │ ├── server_key.pem │ ├── serverpw.pem │ ├── serverpw_key.pem │ ├── servertesting.py │ ├── speed.py │ ├── stress.py │ ├── testConfig.py │ ├── testConnection.py │ ├── testConversionSupport.py │ ├── testTransactionBuffer.py │ ├── testZEO.py │ ├── testZEO2.py │ ├── testZEOOptions.py │ ├── testZEOServer.py │ ├── test_cache.py │ ├── test_client_credentials.py │ ├── test_client_side_conflict_resolution.py │ ├── test_marshal.py │ ├── test_sync.py │ ├── testssl.py │ ├── threaded.py │ ├── utils.py │ ├── zdoptions.test │ ├── zeo-fan-out.test │ └── zeo_blob_cache.test │ ├── util.py │ ├── version.txt │ ├── zconfig.py │ ├── zeoctl.py │ └── zeoctl.xml └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | # 4 | # EditorConfig Configuration file, for more details see: 5 | # http://EditorConfig.org 6 | # EditorConfig is a convention description, that could be interpreted 7 | # by multiple editors to enforce common coding conventions for specific 8 | # file types 9 | 10 | # top-most EditorConfig file: 11 | # Will ignore other EditorConfig files in Home directory or upper tree level. 12 | root = true 13 | 14 | 15 | [*] # For All Files 16 | # Unix-style newlines with a newline ending every file 17 | end_of_line = lf 18 | insert_final_newline = true 19 | trim_trailing_whitespace = true 20 | # Set default charset 21 | charset = utf-8 22 | # Indent style default 23 | indent_style = space 24 | # Max Line Length - a hard line wrap, should be disabled 25 | max_line_length = off 26 | 27 | [*.{py,cfg,ini}] 28 | # 4 space indentation 29 | indent_size = 4 30 | 31 | [*.{yml,zpt,pt,dtml,zcml}] 32 | # 2 space indentation 33 | indent_size = 2 34 | 35 | [{Makefile,.gitmodules}] 36 | # Tab indentation (no size specified, but view as 4 spaces) 37 | indent_style = tab 38 | indent_size = unset 39 | tab_width = unset 40 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | name: pre-commit 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - master 10 | # Allow to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | env: 14 | FORCE_COLOR: 1 15 | 16 | jobs: 17 | pre-commit: 18 | permissions: 19 | contents: read 20 | pull-requests: write 21 | name: linting 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: 3.x 28 | - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd #v3.0.1 29 | with: 30 | extra_args: --all-files --show-diff-on-failure 31 | env: 32 | PRE_COMMIT_COLOR: always 33 | - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 #v1.1.0 34 | if: always() 35 | with: 36 | msg: Apply pre-commit code formatting 37 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | name: tests 4 | 5 | on: 6 | push: 7 | pull_request: 8 | schedule: 9 | - cron: '0 12 * * 0' # run once a week on Sunday 10 | # Allow to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | permissions: 16 | contents: read 17 | pull-requests: write 18 | strategy: 19 | # We want to see all failures: 20 | fail-fast: false 21 | matrix: 22 | os: 23 | - ["ubuntu", "ubuntu-latest"] 24 | config: 25 | # [Python version, tox env] 26 | - ["3.11", "release-check"] 27 | - ["3.9", "py39"] 28 | - ["3.10", "py310"] 29 | - ["3.11", "py311"] 30 | - ["3.12", "py312"] 31 | - ["3.13", "py313"] 32 | - ["pypy-3.10", "pypy3"] 33 | - ["3.11", "docs"] 34 | - ["3.11", "coverage"] 35 | - ["3.9", "py39-msgpack1"] 36 | - ["3.9", "py39-uvloop"] 37 | - ["3.10", "py310-msgpack1"] 38 | - ["3.10", "py310-uvloop"] 39 | - ["3.11", "py311-msgpack1"] 40 | - ["3.11", "py311-uvloop"] 41 | - ["3.12", "py312-msgpack1"] 42 | - ["3.12", "py312-uvloop"] 43 | - ["3.13", "py313-msgpack1"] 44 | - ["3.13", "py313-uvloop"] 45 | - ["3.13", "py313-zodbmaster"] 46 | - ["pypy-3.10", "pypy3-msgpack1"] 47 | 48 | runs-on: ${{ matrix.os[1] }} 49 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 50 | name: ${{ matrix.config[1] }} 51 | steps: 52 | - uses: actions/checkout@v4 53 | with: 54 | persist-credentials: false 55 | - name: Install uv + caching 56 | uses: astral-sh/setup-uv@v6 57 | with: 58 | enable-cache: true 59 | cache-dependency-glob: | 60 | setup.* 61 | tox.ini 62 | python-version: ${{ matrix.matrix.config[0] }} 63 | github-token: ${{ secrets.GITHUB_TOKEN }} 64 | - name: Test 65 | if: ${{ !startsWith(runner.os, 'Mac') }} 66 | run: uvx --with tox-uv tox -e ${{ matrix.config[1] }} 67 | - name: Test (macOS) 68 | if: ${{ startsWith(runner.os, 'Mac') }} 69 | run: uvx --with tox-uv tox -e ${{ matrix.config[1] }}-universal2 70 | - name: Coverage 71 | if: matrix.config[1] == 'coverage' 72 | run: | 73 | uvx coveralls --service=github 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | *.dll 4 | *.egg-info/ 5 | *.profraw 6 | *.pyc 7 | *.pyo 8 | *.so 9 | .coverage 10 | .coverage.* 11 | .eggs/ 12 | .installed.cfg 13 | .mr.developer.cfg 14 | .tox/ 15 | .vscode/ 16 | __pycache__/ 17 | bin/ 18 | build/ 19 | coverage.xml 20 | develop-eggs/ 21 | develop/ 22 | dist/ 23 | docs/_build 24 | eggs/ 25 | etc/ 26 | lib/ 27 | lib64 28 | log/ 29 | parts/ 30 | pyvenv.cfg 31 | testing.log 32 | var/ 33 | -------------------------------------------------------------------------------- /.meta.toml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | [meta] 4 | template = "pure-python" 5 | commit-id = "09d4ab7e" 6 | 7 | [python] 8 | with-windows = false 9 | with-pypy = true 10 | with-future-python = false 11 | with-docs = true 12 | with-sphinx-doctests = false 13 | with-macos = false 14 | 15 | [tox] 16 | testenv-commands = [ 17 | "# Run unit tests first.", 18 | "zope-testrunner -u --test-path=src -a 1000 {posargs:-vc}", 19 | "# Only run functional tests if unit tests pass.", 20 | "zope-testrunner -f --test-path=src -a 1000 {posargs:-vc}", 21 | ] 22 | testenv-deps = [ 23 | "!zodbmaster: ZODB >= 4.2.0b1", 24 | "zodbmaster: git+https://github.com/zopefoundation/ZODB.git@master\\#egg=ZODB", 25 | "uvloop: uvloop", 26 | ] 27 | testenv-setenv = [ 28 | "msgpack1: ZEO_MSGPACK=1", 29 | ] 30 | 31 | [flake8] 32 | additional-config = [ 33 | "# E501 line too long", 34 | "per-file-ignores =", 35 | " src/ZEO/tests/test_cache.py: E501", 36 | " src/ZEO/asyncio/compat.py: F401", 37 | ] 38 | 39 | [coverage] 40 | fail-under = 53 41 | 42 | [manifest] 43 | additional-rules = [ 44 | "include *.py", 45 | "include *.yaml", 46 | "include COPYING", 47 | "include log.ini", 48 | "recursive-include src *.csr", 49 | "recursive-include src *.pem", 50 | "recursive-include src *.pyx", 51 | "recursive-include src *.rst", 52 | "recursive-include src *.test", 53 | "recursive-include src *.txt", 54 | "recursive-include src *.xml", 55 | ] 56 | 57 | [github-actions] 58 | additional-config = [ 59 | "- [\"3.9\", \"py39-msgpack1\"]", 60 | "- [\"3.9\", \"py39-uvloop\"]", 61 | "- [\"3.10\", \"py310-msgpack1\"]", 62 | "- [\"3.10\", \"py310-uvloop\"]", 63 | "- [\"3.11\", \"py311-msgpack1\"]", 64 | "- [\"3.11\", \"py311-uvloop\"]", 65 | "- [\"3.12\", \"py312-msgpack1\"]", 66 | "- [\"3.12\", \"py312-uvloop\"]", 67 | "- [\"3.13\", \"py313-msgpack1\"]", 68 | "- [\"3.13\", \"py313-uvloop\"]", 69 | "- [\"3.13\", \"py313-zodbmaster\"]", 70 | "- [\"pypy-3.10\", \"pypy3-msgpack1\"]", 71 | ] 72 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | minimum_pre_commit_version: '3.6' 4 | repos: 5 | - repo: https://github.com/pycqa/isort 6 | rev: "6.0.1" 7 | hooks: 8 | - id: isort 9 | - repo: https://github.com/hhatto/autopep8 10 | rev: "v2.3.2" 11 | hooks: 12 | - id: autopep8 13 | args: [--in-place, --aggressive, --aggressive] 14 | - repo: https://github.com/asottile/pyupgrade 15 | rev: v3.19.1 16 | hooks: 17 | - id: pyupgrade 18 | args: [--py39-plus] 19 | - repo: https://github.com/isidentical/teyit 20 | rev: 0.4.3 21 | hooks: 22 | - id: teyit 23 | - repo: https://github.com/PyCQA/flake8 24 | rev: "7.2.0" 25 | hooks: 26 | - id: flake8 27 | additional_dependencies: 28 | - flake8-debugger == 4.1.2 29 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | # Read the Docs configuration file 4 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 5 | 6 | # Required 7 | version: 2 8 | 9 | # Set the version of Python and other tools you might need 10 | build: 11 | os: ubuntu-22.04 12 | tools: 13 | python: "3.11" 14 | 15 | # Build documentation in the docs/ directory with Sphinx 16 | sphinx: 17 | configuration: docs/conf.py 18 | 19 | # We recommend specifying your dependencies to enable reproducible builds: 20 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 21 | python: 22 | install: 23 | - requirements: docs/requirements.txt 24 | - method: pip 25 | path: . 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 5 | # Contributing to zopefoundation projects 6 | 7 | The projects under the zopefoundation GitHub organization are open source and 8 | welcome contributions in different forms: 9 | 10 | * bug reports 11 | * code improvements and bug fixes 12 | * documentation improvements 13 | * pull request reviews 14 | 15 | For any changes in the repository besides trivial typo fixes you are required 16 | to sign the contributor agreement. See 17 | https://www.zope.dev/developer/becoming-a-committer.html for details. 18 | 19 | Please visit our [Developer 20 | Guidelines](https://www.zope.dev/developer/guidelines.html) if you'd like to 21 | contribute code changes and our [guidelines for reporting 22 | bugs](https://www.zope.dev/developer/reporting-bugs.html) if you want to file a 23 | bug report. 24 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | See: 2 | 3 | - the copyright notice in: COPYRIGHT.txt 4 | 5 | - The Zope Public License in LICENSE.txt 6 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Zope Foundation and Contributors -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Zope Public License (ZPL) Version 2.1 2 | 3 | A copyright notice accompanies this license document that identifies the 4 | copyright holders. 5 | 6 | This license has been certified as open source. It has also been designated as 7 | GPL compatible by the Free Software Foundation (FSF). 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | 1. Redistributions in source code must retain the accompanying copyright 13 | notice, this list of conditions, and the following disclaimer. 14 | 15 | 2. Redistributions in binary form must reproduce the accompanying copyright 16 | notice, this list of conditions, and the following disclaimer in the 17 | documentation and/or other materials provided with the distribution. 18 | 19 | 3. Names of the copyright holders must not be used to endorse or promote 20 | products derived from this software without prior written permission from the 21 | copyright holders. 22 | 23 | 4. The right to distribute this software or to use it for any purpose does not 24 | give you the right to use Servicemarks (sm) or Trademarks (tm) of the 25 | copyright 26 | holders. Use of them is covered by separate agreement with the copyright 27 | holders. 28 | 29 | 5. If any files are modified, you must cause the modified files to carry 30 | prominent notices stating that you changed the files and the date of any 31 | change. 32 | 33 | Disclaimer 34 | 35 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED 36 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 37 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 38 | EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, 39 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 40 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 41 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 42 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 43 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 44 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 45 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | include *.md 4 | include *.rst 5 | include *.txt 6 | include buildout.cfg 7 | include tox.ini 8 | include .pre-commit-config.yaml 9 | 10 | recursive-include docs *.py 11 | recursive-include docs *.rst 12 | recursive-include docs *.txt 13 | recursive-include docs Makefile 14 | 15 | recursive-include src *.py 16 | include *.py 17 | include *.yaml 18 | include COPYING 19 | include log.ini 20 | recursive-include src *.csr 21 | recursive-include src *.pem 22 | recursive-include src *.pyx 23 | recursive-include src *.rst 24 | recursive-include src *.test 25 | recursive-include src *.txt 26 | recursive-include src *.xml 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================================================ 2 | ZEO - Single-server client-server database server for ZODB 3 | ============================================================ 4 | 5 | ZEO is a client-server storage for `ZODB `_ for 6 | sharing a single storage among many clients. When you use ZEO, a 7 | lower-level storage, typically a file storage, is opened in the ZEO 8 | server process. Client programs connect to this process using a ZEO 9 | ClientStorage. ZEO provides a consistent view of the database to all 10 | clients. The ZEO client and server communicate using a custom 11 | protocol layered on top of TCP. 12 | 13 | Some alternatives to ZEO: 14 | 15 | - `NEO `_ is a distributed-server 16 | client-server storage. 17 | 18 | - `RelStorage `_ 19 | leverages the RDBMS servers to provide a client-server storage. 20 | 21 | The documentation is available on `readthedocs `_. 22 | -------------------------------------------------------------------------------- /buildout.cfg: -------------------------------------------------------------------------------- 1 | [buildout] 2 | develop = . 3 | parts = 4 | test 5 | scripts 6 | versions = versions 7 | extra = 8 | 9 | [versions] 10 | 11 | 12 | [test] 13 | recipe = zc.recipe.testrunner 14 | eggs = 15 | ZEO [test${buildout:extra}] 16 | # ZopeUndo is needed as soft-dependency for a regression test 17 | ZopeUndo 18 | initialization = 19 | import os, tempfile 20 | try: os.mkdir('tmp') 21 | except: pass 22 | tempfile.tempdir = os.path.abspath('tmp') 23 | defaults = ['--all'] 24 | 25 | [scripts] 26 | recipe = zc.recipe.egg 27 | eggs = ${test:eggs} 28 | interpreter = py 29 | -------------------------------------------------------------------------------- /docs/blob-nfs.rst: -------------------------------------------------------------------------------- 1 | =========================================== 2 | How to use NFS to make Blobs more efficient 3 | =========================================== 4 | 5 | :Author: Christian Theune 6 | 7 | Overview 8 | ======== 9 | 10 | When handling blobs, the biggest goal is to avoid writing operations that 11 | require the blob data to be transferred using up IO resources. 12 | 13 | When bringing a blob into the system, at least one O(N) operation has to 14 | happen, e.g. when the blob is uploaded via a network server. The blob should 15 | be extracted as a file on the final storage volume as early as possible, 16 | avoiding further copies. 17 | 18 | In a ZEO setup, all data is stored on a networked server and passed to it 19 | using zrpc. This is a major problem for handling blobs, because it will lock 20 | all transactions from committing when storing a single large blob. As a 21 | default, this mechanism works but is not recommended for high-volume 22 | installations. 23 | 24 | Shared filesystem 25 | ================= 26 | 27 | The solution for the transfer problem is to setup various storage parameters 28 | so that blobs are always handled on a single volume that is shared via network 29 | between ZEO servers and clients. 30 | 31 | Step 1: Setup a writable shared filesystem for ZEO server and client 32 | -------------------------------------------------------------------- 33 | 34 | On the ZEO server, create two directories on the volume that will be used by 35 | this setup (assume the volume is accessible via $SERVER/): 36 | 37 | - $SERVER/blobs 38 | 39 | - $SERVER/tmp 40 | 41 | Then export the $SERVER directory using a shared network filesystem like NFS. 42 | Make sure it's writable by the ZEO clients. 43 | 44 | Assume the exported directory is available on the client as $CLIENT. 45 | 46 | Step 2: Application temporary directories 47 | ----------------------------------------- 48 | 49 | Applications (i.e. Zope) will put uploaded data in a temporary directory 50 | first. Adjust your TMPDIR, TMP or TEMP environment variable to point to the 51 | shared filesystem: 52 | 53 | $ export TMPDIR=$CLIENT/tmp 54 | 55 | Step 3: ZEO client caches 56 | ------------------------- 57 | 58 | Edit the file `zope.conf` on the ZEO client and adjust the configuration of 59 | the `zeoclient` storage with two new variables:: 60 | 61 | blob-dir = $CLIENT/blobs 62 | blob-cache-writable = yes 63 | 64 | Step 4: ZEO server 65 | ------------------ 66 | 67 | Edit the file `zeo.conf` on the ZEO server to configure the blob directory. 68 | Assuming the published storage of the ZEO server is a file storage, then the 69 | configuration should look like this:: 70 | 71 | 72 | 73 | path $INSTANCE/var/Data.fs 74 | 75 | blob-dir $SERVER/blobs 76 | 77 | 78 | (Remember to manually replace $SERVER and $CLIENT with the exported directory 79 | as accessible by either the ZEO server or the ZEO client.) 80 | 81 | Conclusion 82 | ---------- 83 | 84 | At this point, after restarting your ZEO server and clients, the blob 85 | directory will be shared and a minimum amount of IO will occur when working 86 | with blobs. 87 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/client-cache.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | ZEO Client Cache 3 | ================ 4 | 5 | The client cache provides a disk based cache for each ZEO client. The 6 | client cache allows reads to be done from local disk rather than by remote 7 | access to the storage server. 8 | 9 | The cache may be persistent or transient. If the cache is persistent, then 10 | the cache file is retained for use after process restarts. A non- 11 | persistent cache uses a temporary file. 12 | 13 | The client cache is managed in a single file, of the specified size. 14 | 15 | The life of the cache is as follows: 16 | 17 | - The cache file is opened (if it already exists), or created and set to 18 | the specified size. 19 | 20 | - Cache records are written to the cache file, as transactions commit 21 | locally, and as data are loaded from the server. 22 | 23 | - Writes are to "the current file position". This is a pointer that 24 | travels around the file, circularly. After a record is written, the 25 | pointer advances to just beyond it. Objects starting at the current 26 | file position are evicted, as needed, to make room for the next record 27 | written. 28 | 29 | A distinct index file is not created, although indexing structures are 30 | maintained in memory while a ClientStorage is running. When a persistent 31 | client cache file is reopened, these indexing structures are recreated 32 | by analyzing the file contents. 33 | 34 | Persistent cache files are created in the directory named in the ``var`` 35 | argument to the ClientStorage, or if ``var`` is None, in the current 36 | working directory. Persistent cache files have names of the form:: 37 | 38 | client-storage.zec 39 | 40 | where: 41 | 42 | client -- the client name, as given by the ClientStorage's ``client`` 43 | argument 44 | 45 | storage -- the storage name, as given by the ClientStorage's ``storage`` 46 | argument; this is typically a string denoting a small integer, 47 | "1" by default 48 | 49 | For example, the cache file for client '8881' and storage 'spam' is named 50 | "8881-spam.zec". 51 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ========================================================== 2 | ZEO - Single-server client-server database server for ZODB 3 | ========================================================== 4 | 5 | ZEO is a client-server storage for `ZODB `_ for 6 | sharing a single storage among many clients. When you use ZEO, a 7 | lower-level storage, typically a file storage, is opened in the ZEO 8 | server process. Client programs connect to this process using a ZEO 9 | ClientStorage. ZEO provides a consistent view of the database to all 10 | clients. The ZEO client and server communicate using a custom 11 | protocol layered on top of TCP. 12 | 13 | ZEO can be installed with pip like most python packages: 14 | 15 | .. code-block:: bash 16 | 17 | pip install zeo 18 | 19 | Some alternatives to ZEO: 20 | 21 | - `NEO `_ is a distributed-server 22 | client-server storage. 23 | 24 | - `RelStorage `_ 25 | leverages the RDBMS servers to provide a client-server storage. 26 | 27 | Content 28 | ======= 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | 33 | introduction 34 | server 35 | clients 36 | protocol 37 | nagios 38 | ordering 39 | client-cache 40 | client-cache-tracing 41 | blob-nfs 42 | changelog 43 | reference 44 | 45 | Indices and tables 46 | ================== 47 | 48 | * :ref:`genindex` 49 | * :ref:`modindex` 50 | * :ref:`search` 51 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Introduction 3 | ============ 4 | 5 | There are several features that affect the behavior of 6 | ZEO. This section describes how a few of these features 7 | work. Subsequent sections describe how to configure every option. 8 | 9 | Client cache 10 | ============ 11 | 12 | Each ZEO client keeps an on-disk cache of recently used data records 13 | to avoid fetching those records from the server each time they are 14 | requested. It is usually faster to read the objects from disk than it 15 | is to fetch them over the network. The cache can also provide 16 | read-only copies of objects during server outages. 17 | 18 | The cache may be persistent or transient. If the cache is persistent, 19 | then the cache files are retained for use after process restarts. A 20 | non-persistent cache uses temporary files that are removed when the 21 | client storage is closed. 22 | 23 | The client cache size is configured when the ClientStorage is created. 24 | The default size is 20MB, but the right size depends entirely on the 25 | particular database. Setting the cache size too small can hurt 26 | performance, but in most cases making it too big just wastes disk 27 | space. 28 | 29 | ZEO uses invalidations for cache consistency. Every time an object is 30 | modified, the server sends a message to each client informing it of 31 | the change. The client will discard the object from its cache when it 32 | receives an invalidation. (It's actually a little more complicated, 33 | but we won't get into that here.) 34 | 35 | Each time a client connects to a server, it must verify that its cache 36 | contents are still valid. (It did not receive any invalidation 37 | messages while it was disconnected.) This involves asking the server 38 | to replay invalidations it missed. If it's been disconnected too long, 39 | it discards its cache. 40 | 41 | 42 | Invalidation queue 43 | ================== 44 | 45 | The ZEO server keeps a queue of recent invalidation messages in 46 | memory. When a client connects to the server, it sends the timestamp 47 | of the most recent invalidation message it has received. If that 48 | message is still in the invalidation queue, then the server sends the 49 | client all the missing invalidations. 50 | 51 | The default size of the invalidation queue is 100. If the 52 | invalidation queue is larger, it will be more likely that a client 53 | that reconnects will be able to verify its cache using the queue. On 54 | the other hand, a large queue uses more memory on the server to store 55 | the message. Invalidation messages tend to be small, perhaps a few 56 | hundred bytes each on average; it depends on the number of objects 57 | modified by a transaction. 58 | 59 | You can also provide an invalidation age when configuring the 60 | server. In this case, if the invalidation queue is too small, but a 61 | client has been disconnected for a time interval that is less than the 62 | invalidation age, then invalidations are replayed by iterating over 63 | the lower-level storage on the server. If the age is too high, and 64 | clients are disconnected for a long time, then this can put a lot of 65 | load on the server. 66 | 67 | Transaction timeouts 68 | ==================== 69 | 70 | A ZEO server can be configured to timeout a transaction if it takes 71 | too long to complete. Only a single transaction can commit at a time; 72 | so if one transaction takes too long, all other clients will be 73 | delayed waiting for it. In the extreme, a client can hang during the 74 | commit process. If the client hangs, the server will be unable to 75 | commit other transactions until it restarts. A well-behaved client 76 | will not hang, but the server can be configured with a transaction 77 | timeout to guard against bugs that cause a client to hang. 78 | 79 | If any transaction exceeds the timeout threshold, the client's 80 | connection to the server will be closed and the transaction aborted. 81 | Once the transaction is aborted, the server can start processing other 82 | client's requests. Most transactions should take very little time to 83 | commit. The timer begins for a transaction after all the data has 84 | been sent to the server. At this point, the cost of commit should be 85 | dominated by the cost of writing data to disk; it should be unusual 86 | for a commit to take longer than 1 second. A transaction timeout of 87 | 30 seconds should tolerate heavy load and slow communications between 88 | client and server, while guarding against hung servers. 89 | 90 | When a transaction times out, the client can be left in an awkward 91 | position. If the timeout occurs during the second phase of the two 92 | phase commit, the client will log a panic message. This should only 93 | cause problems if the client transaction involved multiple storages. 94 | If it did, it is possible that some storages committed the client 95 | changes and others did not. 96 | 97 | Connection management 98 | ===================== 99 | 100 | A ZEO client manages its connection to the ZEO server. If it loses 101 | the connection, it attempts to reconnect. While 102 | it is disconnected, it can satisfy some reads by using its cache. 103 | 104 | The client can be configured with multiple server addresses. In this 105 | case, it assumes that each server has identical content and will use 106 | any server that is available. It is possible to configure the client 107 | to accept a read-only connection to one of these servers if no 108 | read-write connection is available. If it has a read-only connection, 109 | it will continue to poll for a read-write connection. 110 | 111 | If a single address resolves to multiple IPv4 or IPv6 addresses, 112 | the client will connect to an arbitrary of these addresses. 113 | 114 | SSL 115 | === 116 | 117 | ZEO supports the use of SSL connections between servers and clients, 118 | including certificate authentication. We're still understanding use 119 | cases for this, so details of operation may change. 120 | -------------------------------------------------------------------------------- /docs/nagios.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../src/ZEO/nagios.rst 2 | -------------------------------------------------------------------------------- /docs/protocol.rst: -------------------------------------------------------------------------------- 1 | ========================================== 2 | ZEO Network Protocol (sans authentication) 3 | ========================================== 4 | 5 | This document describes the ZEO network protocol. It assumes that the 6 | optional authentication protocol isn't used. At the lowest 7 | level, the protocol consists of sized messages. All communication 8 | between the client and server consists of sized messages. A sized 9 | message consists of a 4-byte unsigned big-endian content length, 10 | followed by the content. There are two subprotocols, for protocol 11 | negotiation, and for normal operation. The normal operation protocol 12 | is a basic RPC protocol. 13 | 14 | In the protocol negotiation phase, the server sends a protocol 15 | identifier to the client. The client chooses a protocol to use to the 16 | server. The client or the server can fail if it doesn't like the 17 | protocol string sent by the other party. After sending their protocol 18 | strings, the client and server switch to RPC mode. 19 | 20 | The RPC protocol uses messages that are pickled tuples consisting of: 21 | 22 | message_id 23 | The message id is used to match replies with requests, allowing 24 | multiple outstanding synchronous requests. 25 | 26 | async_flag 27 | An integer 0 for a regular (2-way) request and 1 for a one-way 28 | request. Two-way requests have a reply. One way requests don't. 29 | ZRS tries to use as many one-way requests as possible to avoid 30 | network round trips. 31 | 32 | name 33 | The name of a method to call. If this is the special string 34 | ".reply", then the message is interpreted as a return from a 35 | synchronous call. 36 | 37 | args 38 | A tuple of positional arguments or returned values. 39 | 40 | After making a connection and negotiating the protocol, the following 41 | interactions occur: 42 | 43 | - The client requests the authentication protocol by calling 44 | getAuthProtocol. For this discussion, we'll assume the server 45 | returns None. Note that if the server doesn't require 46 | authentication, this step is optional. 47 | 48 | - The client calls register passing a storage identifier and a 49 | read-only flag. The server doesn't return a value, but it may raise 50 | an exception either if the storage doesn't exist, or if the 51 | storage is readonly and the read-only flag passed by the client is 52 | false. 53 | 54 | At this point, the client and server send each other messages as 55 | needed. The client may make regular or one-way calls to the 56 | server. The server sends replies and one-way calls to the client. 57 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Reference 3 | ========= 4 | 5 | Client & server 6 | =============== 7 | 8 | .. autofunction:: ZEO.server 9 | 10 | .. autofunction:: ZEO.client 11 | 12 | .. autoclass:: ZEO.ClientStorage.ClientStorage 13 | :members: 14 | :show-inheritance: 15 | 16 | .. autofunction:: ZEO.DB 17 | 18 | Exceptions 19 | ========== 20 | 21 | .. automodule:: ZEO.Exceptions 22 | :members: 23 | :show-inheritance: 24 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | repoze.sphinx.autointerface 3 | sphinx_rtd_theme > 1 -------------------------------------------------------------------------------- /log.ini: -------------------------------------------------------------------------------- 1 | # This file configures the logging module for the test harness: 2 | # critical errors are logged to testing.log; everything else is 3 | # ignored. 4 | 5 | # Documentation for the file format is at 6 | # http://www.red-dove.com/python_logging.html#config 7 | 8 | [logger_root] 9 | level=CRITICAL 10 | handlers=normal 11 | 12 | [handler_normal] 13 | class=FileHandler 14 | level=NOTSET 15 | formatter=common 16 | args=('testing.log', 'a') 17 | filename=testing.log 18 | mode=a 19 | 20 | [formatter_common] 21 | format=------ 22 | %(asctime)s %(levelname)s %(name)s %(message)s 23 | datefmt=%Y-%m-%dT%H:%M:%S 24 | 25 | [loggers] 26 | keys=root 27 | 28 | [handlers] 29 | keys=normal 30 | 31 | [formatters] 32 | keys=common 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | 4 | [build-system] 5 | requires = [ 6 | "setuptools == 75.8.2", 7 | "wheel", 8 | ] 9 | build-backend = "setuptools.build_meta" 10 | 11 | [tool.coverage.run] 12 | branch = true 13 | source = ["ZEO"] 14 | 15 | [tool.coverage.report] 16 | fail_under = 53 17 | precision = 2 18 | ignore_errors = true 19 | show_missing = true 20 | exclude_lines = [ 21 | "pragma: no cover", 22 | "pragma: nocover", 23 | "except ImportError:", 24 | "raise NotImplementedError", 25 | "if __name__ == '__main__':", 26 | "self.fail", 27 | "raise AssertionError", 28 | "raise unittest.Skip", 29 | ] 30 | 31 | [tool.coverage.html] 32 | directory = "parts/htmlcov" 33 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | 4 | [flake8] 5 | doctests = 1 6 | # E501 line too long 7 | per-file-ignores = 8 | src/ZEO/tests/test_cache.py: E501 9 | src/ZEO/asyncio/compat.py: F401 10 | 11 | [check-manifest] 12 | ignore = 13 | .editorconfig 14 | .meta.toml 15 | docs/_build/html/_sources/* 16 | 17 | [isort] 18 | force_single_line = True 19 | combine_as_imports = True 20 | sections = FUTURE,STDLIB,THIRDPARTY,ZOPE,FIRSTPARTY,LOCALFOLDER 21 | known_third_party = docutils, pkg_resources, pytz 22 | known_zope = 23 | known_first_party = 24 | default_section = ZOPE 25 | line_length = 79 26 | lines_after_imports = 2 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2002, 2003 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | 15 | from setuptools import find_packages 16 | from setuptools import setup 17 | 18 | 19 | version = '6.1.dev0' 20 | 21 | install_requires = [ 22 | 'ZODB >= 5.1.1', 23 | 'transaction >= 2.0.3', 24 | 'persistent >= 4.1.0', 25 | 'zc.lockfile', 26 | 'ZConfig', 27 | 'zdaemon', 28 | 'zope.interface', 29 | ] 30 | 31 | tests_require = [ 32 | # We rely on implementation details of 33 | # test mocks. See https://github.com/zopefoundation/ZODB/pull/222 34 | 'ZConfig', 35 | 'ZODB >= 5.5.1', 36 | 'ZopeUndo', 37 | 'zope.testing', 38 | 'transaction', 39 | 'msgpack', 40 | 'zdaemon', 41 | 'zope.testrunner', 42 | ] 43 | 44 | 45 | long_description = ( 46 | open('README.rst').read() 47 | + '\n' + 48 | open('CHANGES.rst').read() 49 | ) 50 | 51 | setup(name="ZEO", 52 | version=version, 53 | description=long_description.split('\n', 2)[1], 54 | long_description=long_description, 55 | url='https://github.com/zopefoundation/ZEO', 56 | author="Zope Foundation and Contributors", 57 | author_email="zodb@googlegroups.com", 58 | keywords=['database', 'zodb'], 59 | packages=find_packages('src'), 60 | package_dir={'': 'src'}, 61 | license="ZPL-2.1", 62 | platforms=["any"], 63 | classifiers=[ 64 | "Intended Audience :: Developers", 65 | "License :: OSI Approved :: Zope Public License", 66 | "Programming Language :: Python :: 3", 67 | "Programming Language :: Python :: 3.9", 68 | "Programming Language :: Python :: 3.10", 69 | "Programming Language :: Python :: 3.11", 70 | "Programming Language :: Python :: 3.12", 71 | "Programming Language :: Python :: 3.13", 72 | "Programming Language :: Python :: Implementation :: CPython", 73 | "Programming Language :: Python :: Implementation :: PyPy", 74 | "Topic :: Database", 75 | "Topic :: Software Development :: Libraries :: Python Modules", 76 | "Operating System :: Microsoft :: Windows", 77 | "Operating System :: Unix", 78 | "Framework :: ZODB", 79 | ], 80 | extras_require={ 81 | 'test': tests_require, 82 | 'uvloop': [ 83 | 'uvloop >=0.5.1' 84 | ], 85 | 'msgpack': [ 86 | 'msgpack-python' 87 | ], 88 | 'docs': [ 89 | 'Sphinx', 90 | 'repoze.sphinx.autointerface', 91 | 'sphinx_rtd_theme', 92 | ], 93 | }, 94 | install_requires=install_requires, 95 | zip_safe=False, 96 | entry_points=""" 97 | [console_scripts] 98 | zeopack = ZEO.scripts.zeopack:main 99 | runzeo = ZEO.runzeo:main 100 | zeoctl = ZEO.zeoctl:main 101 | zeo-nagios = ZEO.nagios:main 102 | """, 103 | include_package_data=True, 104 | python_requires='>=3.9', 105 | ) 106 | -------------------------------------------------------------------------------- /src/ZEO/Exceptions.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """Exceptions for ZEO.""" 15 | 16 | import transaction.interfaces 17 | from ZODB.POSException import StorageError 18 | 19 | 20 | class ClientStorageError(StorageError): 21 | """An error occurred in the ZEO Client Storage. 22 | """ 23 | 24 | 25 | class UnrecognizedResult(ClientStorageError): 26 | """A server call returned an unrecognized result. 27 | """ 28 | 29 | 30 | class ClientDisconnected(ClientStorageError, 31 | transaction.interfaces.TransientError): 32 | """The database storage is disconnected from the storage. 33 | """ 34 | 35 | 36 | class ProtocolError(ClientStorageError): 37 | """A client contacted a server with an incomparible protocol 38 | """ 39 | 40 | 41 | class ServerException(ClientStorageError): 42 | """ 43 | """ 44 | -------------------------------------------------------------------------------- /src/ZEO/TransactionBuffer.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """A TransactionBuffer store transaction updates until commit or abort. 15 | 16 | A transaction may generate enough data that it is not practical to 17 | always hold pending updates in memory. Instead, a TransactionBuffer 18 | is used to store the data until a commit or abort. 19 | """ 20 | 21 | # A faster implementation might store trans data in memory until it 22 | # reaches a certain size. 23 | 24 | import tempfile 25 | 26 | from zodbpickle.pickle import Pickler 27 | from zodbpickle.pickle import Unpickler 28 | 29 | 30 | class TransactionBuffer: 31 | 32 | # The TransactionBuffer is used by client storage to hold update 33 | # data until the tpc_finish(). It is only used by a single 34 | # thread, because only one thread can be in the two-phase commit 35 | # at one time. 36 | 37 | def __init__(self, connection_generation): 38 | self.connection_generation = connection_generation 39 | self.file = tempfile.TemporaryFile(suffix=".tbuf") 40 | self.count = 0 41 | self.size = 0 42 | self.blobs = [] 43 | # It's safe to use a fast pickler because the only objects 44 | # stored are builtin types -- strings or None. 45 | self.pickler = Pickler(self.file, 1) 46 | self.pickler.fast = 1 47 | self.server_resolved = set() # {oid} 48 | self.client_resolved = {} # {oid -> buffer_record_number} 49 | self.exception = None 50 | 51 | def close(self): 52 | self.file.close() 53 | 54 | def store(self, oid, data): 55 | """Store oid, version, data for later retrieval""" 56 | self.pickler.dump((oid, data)) 57 | self.count += 1 58 | # Estimate per-record cache size 59 | self.size = self.size + (data and len(data) or 0) + 31 60 | 61 | def resolve(self, oid, data): 62 | """Record client-resolved data 63 | """ 64 | self.store(oid, data) 65 | self.client_resolved[oid] = self.count - 1 66 | 67 | def server_resolve(self, oid): 68 | self.server_resolved.add(oid) 69 | 70 | def storeBlob(self, oid, blobfilename): 71 | self.blobs.append((oid, blobfilename)) 72 | 73 | def __iter__(self): 74 | self.file.seek(0) 75 | unpickler = Unpickler(self.file) 76 | server_resolved = self.server_resolved 77 | client_resolved = self.client_resolved 78 | 79 | # Gaaaa, this is awkward. There can be entries in serials that 80 | # aren't in the buffer, because undo. Entries can be repeated 81 | # in the buffer, because ZODB. (Maybe this is a bug now, but 82 | # it may be a feature later. 83 | 84 | seen = set() 85 | for i in range(self.count): 86 | oid, data = unpickler.load() 87 | if client_resolved.get(oid, i) == i: 88 | seen.add(oid) 89 | yield oid, data, oid in server_resolved 90 | 91 | # We may have leftover oids because undo 92 | for oid in server_resolved: 93 | if oid not in seen: 94 | yield oid, None, True 95 | -------------------------------------------------------------------------------- /src/ZEO/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """ZEO -- Zope Enterprise Objects. 15 | 16 | See the file README.txt in this directory for an overview. 17 | 18 | ZEO is now part of ZODB; ZODB's home on the web is 19 | 20 | http://wiki.zope.org/ZODB 21 | 22 | """ 23 | 24 | 25 | def client(*args, **kw): 26 | """ 27 | Shortcut for :class:`ZEO.ClientStorage.ClientStorage`. 28 | """ 29 | import ZEO.ClientStorage 30 | return ZEO.ClientStorage.ClientStorage(*args, **kw) 31 | 32 | 33 | def DB(*args, **kw): 34 | """ 35 | Shortcut for creating a :class:`ZODB.DB` using a ZEO :func:`~ZEO.client`. 36 | """ 37 | s = client(*args, **kw) 38 | try: 39 | import ZODB 40 | return ZODB.DB(s) 41 | except Exception: 42 | s.close() 43 | raise 44 | 45 | 46 | def connection(*args, **kw): 47 | db = DB(*args, **kw) 48 | try: 49 | return db.open_then_close_db_when_connection_closes() 50 | except Exception: 51 | db.close() 52 | raise 53 | 54 | 55 | def server(path=None, blob_dir=None, storage_conf=None, zeo_conf=None, 56 | port=0, threaded=True, **kw): 57 | """Convenience function to start a server for interactive exploration 58 | 59 | This fuction starts a ZEO server, given a storage configuration or 60 | a file-storage path and blob directory. You can also supply a ZEO 61 | configuration string or a port. If neither a ZEO port or 62 | configuration is supplied, a port is chosen randomly. 63 | 64 | The server address and a stop function are returned. The address 65 | can be passed to ZEO.ClientStorage.ClientStorage or ZEO.DB to 66 | create a client to the server. The stop function can be called 67 | without arguments to stop the server. 68 | 69 | Arguments: 70 | 71 | path 72 | A file-storage path. This argument is ignored if a storage 73 | configuration is supplied. 74 | 75 | blob_dir 76 | A blob directory path. This argument is ignored if a storage 77 | configuration is supplied. 78 | 79 | storage_conf 80 | A storage configuration string. If none is supplied, then at 81 | least a file-storage path must be supplied and the storage 82 | configuration will be generated from the file-storage path and 83 | the blob directory. 84 | 85 | zeo_conf 86 | A ZEO server configuration string. 87 | 88 | port 89 | If no ZEO configuration is supplied, the one will be computed 90 | from the port. If no port is supplied, one will be chosedn 91 | dynamically. 92 | 93 | """ 94 | import ZEO._forker as forker 95 | if storage_conf is None and path is None: 96 | storage_conf = '\n' 97 | 98 | return forker.start_zeo_server( 99 | storage_conf, zeo_conf, port, keep=True, path=path, 100 | blob_dir=blob_dir, suicide=False, threaded=threaded, **kw) 101 | -------------------------------------------------------------------------------- /src/ZEO/_compat.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """Python versions compatiblity 15 | """ 16 | import platform 17 | import sys 18 | 19 | from zodbpickle.pickle import Unpickler as _Unpickler 20 | 21 | 22 | PYPY = getattr(platform, 'python_implementation', lambda: None)() == 'PyPy' 23 | WIN = sys.platform.startswith('win') 24 | 25 | 26 | class Unpickler(_Unpickler): 27 | # Python doesn't allow assignments to find_global any more, 28 | # instead, find_class can be overridden 29 | 30 | find_global = None 31 | 32 | def find_class(self, modulename, name): 33 | if self.find_global is None: 34 | return super().find_class(modulename, name) 35 | return self.find_global(modulename, name) 36 | -------------------------------------------------------------------------------- /src/ZEO/asyncio/README.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | asyncio-based networking for ZEO 3 | ================================ 4 | 5 | This package provides the networking interface for ZEO. It provides a 6 | somewhat RPC-like API. 7 | 8 | Notes 9 | ===== 10 | 11 | Sending data immediately: asyncio vs asyncore 12 | -------------------------------------------- 13 | 14 | The previous ZEO networking implementation used the ``asyncore`` library. 15 | When writing with asyncore, writes were done only from the event loop. 16 | This meant that when sending data, code would have to "wake up" the 17 | event loop, typically after adding data to some sort of output buffer. 18 | 19 | Asyncio takes an entirely different and saner approach. When an 20 | application wants to send data, it writes to a transport. All 21 | interactions with a transport (in a correct application) are from the 22 | same thread, which is also the thread running any event loop. 23 | Transports are always either idle or sending data. When idle, the 24 | transport writes to the outout socket immediately. If not all data 25 | isn't sent, then it buffers it and becomes sending. If a transport is 26 | sending, then we know that the socket isn't ready for more data, so 27 | ``write`` can just buffer the data. There's no point in waking up the 28 | event loop, because the socket will do so when it's ready for more 29 | data. 30 | 31 | An exception to the paragraph above occurs when operations cross 32 | threads, as occures for most client operations and when a transaction 33 | commits on the server and results have to be sent to other clients. In 34 | these cases, a call_soon_threadsafe method is used which queues an 35 | operation and has to wake up an event loop to process it. 36 | 37 | Server threading 38 | ---------------- 39 | 40 | ZEO server implementation always uses single networking thread that serves all 41 | clients. In other words the server is single-threaded. 42 | 43 | Historically ZEO switched to a multi-threaded implementation several years ago 44 | because it was found to improve performance for large databases using 45 | magnetic disks. Because client threads are always working on behalf of 46 | a single client, there's not really an issue with making blocking 47 | calls, such as executing slow I/O operations. 48 | 49 | Initially, the asyncio-based implementation used a multi-threaded 50 | server. A simple thread accepted connections and handed accepted 51 | sockets to ``create_connection``. This became a problem when SSL was 52 | added because ``create_connection`` sets up SSL conections as client 53 | connections, and doesn't provide an option to create server 54 | connections. 55 | 56 | In response, Jim created an ``asyncio.Server``-based implementation. 57 | This required using a single thread. This was a pretty trivial 58 | change, however, it led to the tests becoming unstable to the point 59 | that it was impossible to run all tests without some failing. One 60 | test was broken due to a ``asyncio.Server`` `bug 61 | `_. It's unclear whether the test 62 | instability is due to ``asyncio.Server`` problems or due to latent 63 | test (or ZEO) bugs, but even after beating the tests mostly into 64 | submission, tests failures are more likely when using 65 | ``asyncio.Server``. Beatings will continue. 66 | 67 | While fighting test failures using ``asyncio.Server``, the 68 | multi-threaded implementation was updated to use a monkey patch to 69 | allow it to create SSL server connections. Aside from the real risk of a 70 | monkey patch, this works very well. 71 | 72 | Both implementations seemed to perform about the same. 73 | 74 | Over the time single-threaded server mode became the default, and, given that 75 | multi-threaded implementation did not provide any speed advantages, it was 76 | eventually deprecated and scheduled for removal to ease maintenance burden. 77 | 78 | Finally, the multi-threaded server mode was removed, when it was found that this 79 | mode had concurrency bugs that lead to data corruptions. See `issue 209 80 | ` for details. 81 | -------------------------------------------------------------------------------- /src/ZEO/asyncio/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /src/ZEO/asyncio/_smp.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=3, boundscheck=False, wraparound=False 2 | 3 | """``cython`` implementation for ``SizedMessageProtocol``.""" 4 | from cpython.bytes cimport PyBytes_FromStringAndSize 5 | from libc.string cimport memcpy 6 | 7 | import logging 8 | import struct 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | cdef enum ReadState: 15 | process_size = 1 16 | process_message = 2 17 | closed = 0 18 | 19 | cdef object pack = struct.pack 20 | cdef object unpack = struct.unpack 21 | 22 | 23 | cdef class SizedMessageProtocol: 24 | cdef object __closed 25 | cdef public object receive # callback for received messages 26 | cdef public object connection_lost_called 27 | 28 | def __init__(self, receive): 29 | self.receive = receive 30 | self.__closed = self.connection_lost_called = False 31 | 32 | def set_receive(self, receive): 33 | self.receive = receive 34 | 35 | cdef public object transport 36 | 37 | def close(self): 38 | if self.__closed: 39 | return 40 | self.__closed = True 41 | self.eof_received() 42 | self.transport.close() 43 | # break reference cycles 44 | self.transport = self.receive = self.writelines = None 45 | 46 | # output 47 | cdef bint paused 48 | cdef list output # buffer 49 | cdef object writelines 50 | 51 | cdef _write_message(self, bytes message): 52 | self.writelines((pack(">I", len(message)), message)) 53 | 54 | def write_message(self, message): 55 | if self.paused: 56 | self.output.append(message) 57 | else: 58 | self._write_message(message) 59 | 60 | def write_message_iter(self, message_iter): 61 | it = iter(message_iter) 62 | if self.paused: 63 | self.output.append(it) 64 | return 65 | for message in it: 66 | self._write_message(message) 67 | if self.paused: 68 | self.output.append(it) 69 | return 70 | 71 | # protocol responsibilities 72 | def pause_writing(self): 73 | self.paused = 1 74 | 75 | def resume_writing(self): 76 | self.paused = 0 77 | cdef list output = self.output 78 | while output and not self.paused: 79 | message = output.pop(0) 80 | if type(message) is bytes: 81 | self._write_message(message) 82 | else: 83 | it = message 84 | for message in it: 85 | self._write_message(message) 86 | if self.paused: 87 | self.output.insert(0, it) 88 | return 89 | 90 | # input 91 | cdef ReadState read_state # current read state 92 | cdef unsigned read_wanted # wanted data size 93 | cdef unsigned received_count # received unprocessed bytes 94 | cdef list chunk_buffer # received data chunks 95 | cdef unsigned chunk_index # unprocessed index in 1. chunk 96 | 97 | # protocol responsibilities 98 | def data_received(self, bytes data): 99 | self.chunk_buffer.append(data) 100 | self.received_count += len(data) 101 | cdef unsigned wanted = self.read_wanted 102 | cdef bytes target 103 | cdef unsigned char *tv 104 | cdef unsigned tvi 105 | cdef bytes chunk 106 | cdef const unsigned char *cv 107 | cdef unsigned ci 108 | cdef unsigned unprocessed, use, i 109 | while self.read_state and self.read_wanted <= self.received_count: 110 | wanted = self.read_wanted 111 | tv = target = PyBytes_FromStringAndSize( NULL, wanted) 112 | tvi = 0 113 | while wanted: 114 | cv = chunk = self.chunk_buffer[0] 115 | ci = self.chunk_index 116 | unprocessed = len(chunk) - ci 117 | if unprocessed > wanted: 118 | use = wanted 119 | self.chunk_index += wanted 120 | else: 121 | use = unprocessed 122 | self.chunk_buffer.pop(0) 123 | self.chunk_index = 0 124 | if use <= 4: 125 | for i in range(use): 126 | tv[tvi + i] = cv[ci + i] 127 | else: 128 | memcpy(&tv[tvi], &cv[ci], use) 129 | tvi += use 130 | wanted -= use 131 | self.received_count -= self.read_wanted 132 | if self.read_state == process_size: 133 | self.read_state = process_message 134 | self.read_wanted = unpack(">I", target)[0] 135 | else: # read_state == process_message 136 | try: 137 | self.receive(target) 138 | except Exception: 139 | logger.exception("Processing message `%r` failed" 140 | % target) 141 | if self.read_state: # not yet closed 142 | self.read_state = process_size 143 | self.read_wanted = 4 144 | 145 | def connection_made(self, transport): 146 | self.transport = transport 147 | self.writelines = transport.writelines 148 | self.paused = 0 149 | self.output = [] 150 | self.read_state = process_size 151 | self.read_wanted = 4 152 | self.received_count = 0 153 | self.chunk_buffer = [] 154 | self.chunk_index = 0 155 | 156 | 157 | def connection_lost(self, exc): 158 | self.connection_lost_called = True 159 | if self.__closed: 160 | return 161 | self.transport.close() 162 | 163 | def eof_received(self): 164 | self.read_state = closed 165 | -------------------------------------------------------------------------------- /src/ZEO/asyncio/base.py: -------------------------------------------------------------------------------- 1 | """ZEO Protocol. 2 | 3 | A ZEO protocol instance can be used as a connection. 4 | It exchanges ``bytes`` messages. 5 | Messages are sent via the methods 6 | ``write_message`` (send a single message) and 7 | ``write_message_iter`` (send the messages generated by an iterator). 8 | Received messages are reported via callbacks. 9 | Messages are received in the same order as they have been written; 10 | especially, the messages wrote with ``write_message_iter`` 11 | are received as contiguous messages. 12 | 13 | The first message transmits the protocol version. 14 | Its callback is ``finish_connection``. 15 | The first byte of the protocol version message identifies 16 | an encoding type; the remaining bytes specify the version. 17 | ``finish_connection`` is expected to set up 18 | methods ``encode`` and ``decode`` corresponding to the 19 | encoding type. 20 | 21 | Followup messages carry encoded tuples 22 | *msgid*, *async_flag*, *name*, *args* 23 | representing either calls (synchronous or asynchronous) or replies. 24 | Their callback is ``message_received``. 25 | 26 | ZEO protocol instances can be used concurrently from coroutines (executed 27 | in the same thread). 28 | They are not thread safe. 29 | 30 | The ZEO protocol sits on top of a sized message protocol. 31 | 32 | The ZEO protocol has client and server variants. 33 | """ 34 | import logging 35 | from asyncio import Protocol 36 | 37 | from .smp import SizedMessageProtocol 38 | 39 | 40 | logger = logging.getLogger(__name__) 41 | 42 | 43 | class ZEOBaseProtocol(Protocol): 44 | """ZEO protocol base class for the common features.""" 45 | 46 | protocol_version = None 47 | 48 | def __init__(self, loop, name): 49 | self.loop = loop 50 | self.name = name 51 | 52 | # API -- defined in ``connection_made`` 53 | # write_message(message) 54 | # write_message_iter(message_iter) 55 | def call_async(self, method, args): 56 | """call method named *method* asynchronously with *args*.""" 57 | self.write_message(self.encode(0, True, method, args)) 58 | 59 | def call_async_iter(self, it): 60 | self.write_message_iter(self.encode(0, True, method, args) 61 | for method, args in it) 62 | 63 | def get_peername(self): 64 | return self.sm_protocol.transport.get_extra_info('peername') 65 | 66 | def protocol_factory(self): 67 | return self 68 | 69 | closing = None # ``None`` or closed future 70 | sm_protocol = None 71 | 72 | def close(self): 73 | """schedule closing, return closed future.""" 74 | # with ``asyncio``, ``close`` only schedules the closing; 75 | # close completion is signalled via a call to ``connection_lost``. 76 | closing = self.closing 77 | if closing is None: 78 | closing = self.closing = self.loop.create_future() 79 | # can get closed before ``sm_protocol`` set up 80 | if self.sm_protocol is not None: 81 | # will eventually cause ``connection_lost`` 82 | self.sm_protocol.close() 83 | else: 84 | closing.set_result(True) 85 | elif self.sm_protocol is not None: 86 | self.sm_protocol.close() # no problem if repeated 87 | return closing 88 | 89 | def __repr__(self): 90 | cls = self.__class__ 91 | return f'{cls.__module__}.{cls.__name__}({self.name})' 92 | 93 | # to be defined by deriving classes 94 | # def finish_connection(protocol_version_message) 95 | # def message_received(message) 96 | 97 | # ``Protocol`` responsibilities -- defined in ``connection_made`` 98 | # data_received 99 | # eof_received 100 | # pause_writing 101 | # resume_writing 102 | def connection_made(self, transport): 103 | logger.info("Connected %s", self) 104 | # set up lower level sized message protocol 105 | # creates reference cycle 106 | smp = self.sm_protocol = SizedMessageProtocol(self._first_message) 107 | smp.connection_made(transport) # takes over ``transport`` 108 | self.data_received = smp.data_received 109 | self.eof_received = smp.eof_received 110 | self.pause_writing = smp.pause_writing 111 | self.resume_writing = smp.resume_writing 112 | self.write_message = smp.write_message 113 | self.write_message_iter = smp.write_message_iter 114 | 115 | # In real life ``connection_lost`` is only called by 116 | # the transport and ``asyncio.Protocol`` guarantees that 117 | # it is called exactly once (if ``connection_made`` has 118 | # been called) or not at all. 119 | # Some tests, however, call ``connection_lost`` themselves. 120 | # The following attribute helps to ensure that ``connection_lost`` 121 | # is called exactly once. 122 | connection_lost_called = False 123 | 124 | def connection_lost(self, exc): 125 | """The call signals close completion.""" 126 | self.connection_lost_called = True 127 | self.sm_protocol.connection_lost(exc) 128 | closing = self.closing 129 | if closing is None: 130 | closing = self.closing = self.loop.create_future() 131 | if not closing.done(): 132 | closing.set_result(True) 133 | 134 | # internal 135 | def _first_message(self, protocol_version): 136 | self.sm_protocol.set_receive(self.message_received) 137 | self.finish_connection(protocol_version) 138 | 139 | # ``uvloop`` workaround 140 | # We define ``data_received`` in ``connection_made``. 141 | # ``uvloop``, however, caches ``protocol.data_received`` before 142 | # it calls ``connection_made`` - at a consequence, data is not 143 | # received 144 | # The method below is overridden in ``connection_made``. 145 | def data_received(self, data): 146 | self.data_received(data) # not an infinite loop, because overridden 147 | -------------------------------------------------------------------------------- /src/ZEO/asyncio/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from uvloop import new_event_loop 3 | except ModuleNotFoundError: 4 | from asyncio import new_event_loop 5 | -------------------------------------------------------------------------------- /src/ZEO/asyncio/marshal.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """Support for marshaling ZEO messages 15 | 16 | Not to be confused with marshaling objects in ZODB. 17 | 18 | We currently use pickle. In the future, we may use a 19 | Python-independent format, or possibly a minimal pickle subset. 20 | """ 21 | 22 | import logging 23 | from io import BytesIO 24 | 25 | from zodbpickle.pickle import Pickler 26 | 27 | from .._compat import Unpickler 28 | from ..shortrepr import short_repr 29 | 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | def encoder(protocol, server=False): 35 | """Return a non-thread-safe encoder 36 | """ 37 | 38 | if protocol[:1] == b'M': 39 | from msgpack import packb 40 | 41 | default = server_default if server else None 42 | 43 | def encode(*args): 44 | return packb( 45 | args, use_bin_type=True, default=default) 46 | 47 | return encode 48 | else: 49 | assert protocol[:1] == b'Z' 50 | 51 | f = BytesIO() 52 | getvalue = f.getvalue 53 | seek = f.seek 54 | truncate = f.truncate 55 | pickler = Pickler(f, 3) 56 | pickler.fast = 1 57 | dump = pickler.dump 58 | 59 | def encode(*args): 60 | seek(0) 61 | truncate() 62 | dump(args) 63 | return getvalue() 64 | 65 | return encode 66 | 67 | 68 | def encode(*args): 69 | 70 | return encoder(b'Z')(*args) 71 | 72 | 73 | def decoder(protocol): 74 | if protocol[:1] == b'M': 75 | from msgpack import unpackb 76 | 77 | def msgpack_decode(data): 78 | """Decodes msg and returns its parts""" 79 | return unpackb(data, raw=False, use_list=False) 80 | 81 | return msgpack_decode 82 | else: 83 | assert protocol[:1] == b'Z' 84 | return pickle_decode 85 | 86 | 87 | def pickle_decode(msg): 88 | """Decodes msg and returns its parts""" 89 | unpickler = Unpickler(BytesIO(msg)) 90 | unpickler.find_global = find_global 91 | try: 92 | # PyPy, zodbpickle, the non-c-accelerated version 93 | unpickler.find_class = find_global 94 | except AttributeError: 95 | pass 96 | try: 97 | return unpickler.load() # msgid, flags, name, args 98 | except: # NOQA: E722 bare except 99 | logger.error("can't decode message: %s" % short_repr(msg)) 100 | raise 101 | 102 | 103 | def server_decoder(protocol): 104 | if protocol[:1] == b'M': 105 | return decoder(protocol) 106 | else: 107 | assert protocol[:1] == b'Z' 108 | return pickle_server_decode 109 | 110 | 111 | def pickle_server_decode(msg): 112 | """Decodes msg and returns its parts""" 113 | unpickler = Unpickler(BytesIO(msg)) 114 | unpickler.find_global = server_find_global 115 | try: 116 | # PyPy, zodbpickle, the non-c-accelerated version 117 | unpickler.find_class = server_find_global 118 | except AttributeError: 119 | pass 120 | 121 | try: 122 | return unpickler.load() # msgid, flags, name, args 123 | except: # NOQA: E722 bare except 124 | logger.error("can't decode message: %s" % short_repr(msg)) 125 | raise 126 | 127 | 128 | def server_default(obj): 129 | if isinstance(obj, Exception): 130 | return reduce_exception(obj) 131 | else: 132 | return obj 133 | 134 | 135 | def reduce_exception(exc): 136 | class_ = exc.__class__ 137 | class_ = f'{class_.__module__}.{class_.__name__}' 138 | return class_, exc.__dict__ or exc.args 139 | 140 | 141 | _globals = globals() 142 | _silly = ('__doc__',) 143 | 144 | exception_type_type = type(Exception) 145 | 146 | _SAFE_MODULE_NAMES = ( 147 | 'ZopeUndo.Prefix', 'zodbpickle', 148 | 'builtins', 'copy_reg', '__builtin__', 149 | ) 150 | 151 | 152 | def find_global(module, name): 153 | """Helper for message unpickler""" 154 | try: 155 | m = __import__(module, _globals, _globals, _silly) 156 | except ImportError as msg: 157 | raise ImportError(f'import error {module}: {msg}') 158 | 159 | try: 160 | r = getattr(m, name) 161 | except AttributeError: 162 | raise ImportError(f'module {module} has no global {name}') 163 | 164 | safe = getattr(r, '__no_side_effects__', 0) 165 | if safe: 166 | return r 167 | 168 | # TODO: is there a better way to do this? 169 | if isinstance(r, exception_type_type) and issubclass(r, Exception): 170 | return r 171 | 172 | raise ImportError(f'Unsafe global: {module}.{name}') 173 | 174 | 175 | def server_find_global(module, name): 176 | """Helper for message unpickler""" 177 | if module not in _SAFE_MODULE_NAMES: 178 | raise ImportError(f'Module not allowed: {module}') 179 | 180 | try: 181 | m = __import__(module, _globals, _globals, _silly) 182 | except ImportError as msg: 183 | raise ImportError(f'import error {module}: {msg}') 184 | 185 | try: 186 | r = getattr(m, name) 187 | except AttributeError: 188 | raise ImportError(f'module {module} has no global {name}') 189 | 190 | return r 191 | -------------------------------------------------------------------------------- /src/ZEO/interfaces.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2006 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | 15 | import zope.interface 16 | 17 | 18 | class StaleCache: 19 | """A ZEO cache is stale and requires verification. 20 | """ 21 | 22 | def __init__(self, storage): 23 | self.storage = storage 24 | 25 | 26 | class IClientCache(zope.interface.Interface): 27 | """Client cache interface. 28 | 29 | Note that caches need to be thread safe. 30 | """ 31 | 32 | def close(): 33 | """Close the cache 34 | """ 35 | 36 | def load(oid): 37 | """Get current data for object 38 | 39 | Returns data and serial, or None. 40 | """ 41 | 42 | def __len__(): 43 | """Retirn the number of items in the cache. 44 | """ 45 | 46 | def store(oid, start_tid, end_tid, data): 47 | """Store data for the object 48 | 49 | The start_tid is the transaction that committed this data. 50 | 51 | The end_tid is the tid of the next transaction that modified 52 | the objects, or None if this is the current version. 53 | """ 54 | 55 | def loadBefore(oid, tid): 56 | """Load the data for the object last modified before the tid 57 | 58 | Returns the data, and start and end tids. 59 | """ 60 | 61 | def invalidate(oid, tid): 62 | """Invalidate data for the object 63 | 64 | If ``tid`` is None, forget all knowledge of `oid`. (``tid`` 65 | can be None only for invalidations generated by startup cache 66 | verification.) 67 | 68 | If ``tid`` isn't None, and we had current data for ``oid``, 69 | stop believing we have current data, and mark the data we had 70 | as being valid only up to `tid`. In all other cases, do 71 | nothing. 72 | """ 73 | 74 | def getLastTid(): 75 | """Get the last tid seen by the cache 76 | 77 | This is the cached last tid we've seen from the server. 78 | 79 | This method may be called from multiple threads. (It's assumed 80 | to be trivial.) 81 | """ 82 | 83 | def setLastTid(tid): 84 | """Save the last tid sent by the server 85 | """ 86 | 87 | def clear(): 88 | """Clear/empty the cache 89 | """ 90 | 91 | 92 | class IServeable(zope.interface.Interface): 93 | """Interface provided by storages that can be served by ZEO 94 | """ 95 | 96 | def tpc_transaction(): 97 | """The current transaction being committed. 98 | 99 | If a storage is participating in a two-phase commit, then 100 | return the transaction (object) being committed. Otherwise 101 | return None. 102 | """ 103 | 104 | def lastInvalidations(size): 105 | """Get recent transaction invalidations 106 | 107 | This method is optional and is used to get invalidations 108 | performed by the most recent transactions. 109 | 110 | An iterable of up to size entries must be returned, where each 111 | entry is a transaction id and a sequence of object-id/empty-string 112 | pairs describing the objects written by the 113 | transaction, in chronological order. 114 | """ 115 | -------------------------------------------------------------------------------- /src/ZEO/monitor.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2003 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """Monitor behavior of ZEO server and record statistics. 15 | """ 16 | 17 | import time 18 | 19 | 20 | zeo_version = 'unknown' 21 | try: 22 | import pkg_resources 23 | except ModuleNotFoundError: 24 | pass 25 | else: 26 | zeo_dist = pkg_resources.working_set.find( 27 | pkg_resources.Requirement.parse('ZODB3') 28 | ) 29 | if zeo_dist is not None: 30 | zeo_version = zeo_dist.version 31 | 32 | 33 | class StorageStats: 34 | """Per-storage usage statistics.""" 35 | 36 | def __init__(self, connections=None): 37 | self.connections = connections 38 | self.loads = 0 39 | self.stores = 0 40 | self.commits = 0 41 | self.aborts = 0 42 | self.active_txns = 0 43 | self.lock_time = None 44 | self.conflicts = 0 45 | self.conflicts_resolved = 0 46 | self.start = time.ctime() 47 | 48 | @property 49 | def clients(self): 50 | return len(self.connections) 51 | 52 | def parse(self, s): 53 | # parse the dump format 54 | lines = s.split("\n") 55 | for line in lines: 56 | field, value = line.split(":", 1) 57 | if field == "Server started": 58 | self.start = value 59 | elif field == "Clients": 60 | # Hack because we use this both on the server and on 61 | # the client where there are no connections. 62 | self.connections = [0] * int(value) 63 | elif field == "Clients verifying": 64 | self.verifying_clients = int(value) 65 | elif field == "Active transactions": 66 | self.active_txns = int(value) 67 | elif field == "Commit lock held for": 68 | # This assumes 69 | self.lock_time = time.time() - int(value) 70 | elif field == "Commits": 71 | self.commits = int(value) 72 | elif field == "Aborts": 73 | self.aborts = int(value) 74 | elif field == "Loads": 75 | self.loads = int(value) 76 | elif field == "Stores": 77 | self.stores = int(value) 78 | elif field == "Conflicts": 79 | self.conflicts = int(value) 80 | elif field == "Conflicts resolved": 81 | self.conflicts_resolved = int(value) 82 | 83 | def dump(self, f): 84 | print("Server started:", self.start, file=f) 85 | print("Clients:", self.clients, file=f) 86 | print("Clients verifying:", self.verifying_clients, file=f) 87 | print("Active transactions:", self.active_txns, file=f) 88 | if self.lock_time: 89 | howlong = time.time() - self.lock_time 90 | print("Commit lock held for:", int(howlong), file=f) 91 | print("Commits:", self.commits, file=f) 92 | print("Aborts:", self.aborts, file=f) 93 | print("Loads:", self.loads, file=f) 94 | print("Stores:", self.stores, file=f) 95 | print("Conflicts:", self.conflicts, file=f) 96 | print("Conflicts resolved:", self.conflicts_resolved, file=f) 97 | -------------------------------------------------------------------------------- /src/ZEO/nagios.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2011 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """%prog [options] address 15 | 16 | Where the address is an IPV6 address of the form: [addr]:port, an IPV4 17 | address of the form: addr:port, or the name of a unix-domain socket file. 18 | """ 19 | import json 20 | import optparse 21 | import os 22 | import re 23 | import socket 24 | import struct 25 | import sys 26 | import time 27 | 28 | 29 | NO_TRANSACTION = '0' * 16 30 | 31 | nodiff_names = 'active_txns connections waiting'.split() 32 | diff_names = 'aborts commits conflicts conflicts_resolved loads stores'.split() 33 | 34 | per_times = dict(seconds=1.0, minutes=60.0, hours=3600.0, days=86400.0) 35 | 36 | 37 | def new_metric(metrics, storage_id, name, value): 38 | if storage_id == '1': 39 | label = name 40 | else: 41 | if ' ' in storage_id: 42 | label = f"'{storage_id}:{name}'" 43 | else: 44 | label = f'{storage_id}:{name}' 45 | metrics.append(f'{label}={value}') 46 | 47 | 48 | def result(messages, metrics=(), status=None): 49 | if metrics: 50 | messages[0] += '|' + metrics[0] 51 | if len(metrics) > 1: 52 | messages.append('| ' + '\n '.join(metrics[1:])) 53 | print('\n'.join(messages)) 54 | return status 55 | 56 | 57 | def error(message): 58 | return result((message, ), (), 2) 59 | 60 | 61 | def warn(message): 62 | return result((message, ), (), 1) 63 | 64 | 65 | def check(addr, output_metrics, status, per): 66 | m = re.match(r'\[(\S+)\]:(\d+)$', addr) 67 | if m: 68 | addr = m.group(1), int(m.group(2)) 69 | s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 70 | else: 71 | m = re.match(r'(\S+):(\d+)$', addr) 72 | if m: 73 | addr = m.group(1), int(m.group(2)) 74 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 75 | else: 76 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 77 | try: 78 | s.connect(addr) 79 | except OSError as err: 80 | s.close() 81 | return error("Can't connect %s" % err) 82 | 83 | s.sendall(b'\x00\x00\x00\x04ruok') 84 | proto = s.recv(struct.unpack(">I", s.recv(4))[0]) # NOQA: F841 unused 85 | datas = s.recv(struct.unpack(">I", s.recv(4))[0]) 86 | s.close() 87 | data = json.loads(datas.decode("ascii")) 88 | if not data: 89 | return warn("No storages") 90 | 91 | metrics = [] 92 | messages = [] 93 | level = 0 94 | if output_metrics: 95 | for storage_id, sdata in sorted(data.items()): 96 | for name in nodiff_names: 97 | new_metric(metrics, storage_id, name, sdata[name]) 98 | 99 | if status: 100 | now = time.time() 101 | if os.path.exists(status): 102 | dt = now - os.stat(status).st_mtime 103 | if dt > 0: # sanity :) 104 | with open(status) as f: # Read previous 105 | old = json.loads(f.read()) 106 | dt /= per_times[per] 107 | for storage_id, sdata in sorted(data.items()): 108 | sdata['sameple-time'] = now 109 | if storage_id in old: 110 | sold = old[storage_id] 111 | for name in diff_names: 112 | v = (sdata[name] - sold[name]) / dt 113 | new_metric(metrics, storage_id, name, v) 114 | with open(status, 'w') as f: # save current 115 | f.write(json.dumps(data)) 116 | 117 | for storage_id, sdata in sorted(data.items()): 118 | if sdata['last-transaction'] == NO_TRANSACTION: 119 | messages.append("Empty storage %r" % storage_id) 120 | level = max(level, 1) 121 | if not messages: 122 | messages.append('OK') 123 | return result(messages, metrics, level or None) 124 | 125 | 126 | def main(args=None): 127 | if args is None: 128 | args = sys.argv[1:] 129 | 130 | parser = optparse.OptionParser(__doc__) 131 | parser.add_option( 132 | '-m', '--output-metrics', action="store_true", 133 | help="Output metrics.", 134 | ) 135 | parser.add_option( 136 | '-s', '--status-path', 137 | help="Path to status file, needed to get rate metrics", 138 | ) 139 | parser.add_option( 140 | '-u', '--time-units', type='choice', default='minutes', 141 | choices=['seconds', 'minutes', 'hours', 'days'], 142 | help="Time unit for rate metrics", 143 | ) 144 | (options, args) = parser.parse_args(args) 145 | [addr] = args 146 | return check( 147 | addr, options.output_metrics, options.status_path, options.time_units) 148 | 149 | 150 | if __name__ == '__main__': 151 | main() 152 | -------------------------------------------------------------------------------- /src/ZEO/nagios.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | ZEO Nagios plugin 3 | ================= 4 | 5 | ZEO includes a script that provides a nagios monitor plugin: 6 | 7 | >>> import pkg_resources, time 8 | >>> nagios = pkg_resources.load_entry_point( 9 | ... 'ZEO', 'console_scripts', 'zeo-nagios') 10 | 11 | In it's simplest form, the script just checks if it can get status: 12 | 13 | >>> import ZEO 14 | >>> addr, stop = ZEO.server('test.fs', threaded=False) 15 | >>> saddr = ':'.join(map(str, addr)) # (host, port) -> host:port 16 | 17 | >>> nagios([saddr]) 18 | Empty storage '1' 19 | 1 20 | 21 | The storage was empty. In that case, the monitor warned as much. 22 | 23 | Let's add some data: 24 | 25 | >>> ZEO.DB(addr).close() 26 | >>> nagios([saddr]) 27 | OK 28 | 29 | If we stop the server, we'll error: 30 | 31 | >>> stop() 32 | >>> nagios([saddr]) 33 | Can't connect [Errno 61] Connection refused 34 | 2 35 | 36 | Metrics 37 | ======= 38 | 39 | The monitor will optionally output server metric data. There are 2 40 | kinds of metrics it can output, level and rate metric. If we use the 41 | -m/--output-metrics option, we'll just get rate metrics: 42 | 43 | >>> addr, stop = ZEO.server('test.fs', threaded=False) 44 | >>> saddr = ':'.join(map(str, addr)) # (host, port) -> host:port 45 | >>> nagios([saddr, '-m']) 46 | OK|active_txns=0 47 | | connections=0 48 | waiting=0 49 | 50 | We only got the metrics that are levels, like current number of 51 | connections. If we want rate metrics, we need to be able to save 52 | values from run to run. We need to use the -s/--status-path option to 53 | specify the name of a file for status information: 54 | 55 | >>> nagios([saddr, '-m', '-sstatus']) 56 | OK|active_txns=0 57 | | connections=0 58 | waiting=0 59 | 60 | We still didn't get any rate metrics, because we've only run once. 61 | Let's actually do something with the database and then make another 62 | sample. 63 | 64 | >>> db = ZEO.DB(addr) 65 | >>> nagios([saddr, '-m', '-sstatus']) 66 | OK|active_txns=0 67 | | connections=1 68 | waiting=0 69 | aborts=0.0 70 | commits=0.0 71 | conflicts=0.0 72 | conflicts_resolved=0.0 73 | loads=81.226297803 74 | stores=0.0 75 | 76 | Note that this time, we saw that there was a connection. 77 | 78 | The ZEO.nagios module provides a check function that can be used by 79 | other monitors (e.g. that get address data from ZooKeeper). It takes: 80 | 81 | - Address string, 82 | 83 | - Metrics flag. 84 | 85 | - Status file name (or None), and 86 | 87 | - Time units for rate metrics 88 | 89 | :: 90 | 91 | >>> import ZEO.nagios 92 | >>> ZEO.nagios.check(saddr, True, 'status', 'seconds') 93 | OK|active_txns=0 94 | | connections=1 95 | waiting=0 96 | aborts=0.0 97 | commits=0.0 98 | conflicts=0.0 99 | conflicts_resolved=0.0 100 | loads=0.0 101 | stores=0.0 102 | 103 | >>> db.close() 104 | >>> stop() 105 | 106 | Multi-storage servers 107 | ===================== 108 | 109 | A ZEO server can host multiple servers. (This is a feature that will 110 | likely be dropped in the future.) When this is the case, the monitor 111 | profixes metrics with a storage id. 112 | 113 | >>> addr, stop = ZEO.server( 114 | ... storage_conf = """ 115 | ... 116 | ... 117 | ... 118 | ... 119 | ... """, threaded=False) 120 | >>> saddr = ':'.join(map(str, addr)) # (host, port) -> host:port 121 | >>> nagios([saddr, '-m', '-sstatus']) 122 | Empty storage 'first'|first:active_txns=0 123 | Empty storage 'second' 124 | | first:connections=0 125 | first:waiting=0 126 | second:active_txns=0 127 | second:connections=0 128 | second:waiting=0 129 | 1 130 | >>> nagios([saddr, '-m', '-sstatus']) 131 | Empty storage 'first'|first:active_txns=0 132 | Empty storage 'second' 133 | | first:connections=0 134 | first:waiting=0 135 | second:active_txns=0 136 | second:connections=0 137 | second:waiting=0 138 | first:aborts=0.0 139 | first:commits=0.0 140 | first:conflicts=0.0 141 | first:conflicts_resolved=0.0 142 | first:loads=42.42 143 | first:stores=0.0 144 | second:aborts=0.0 145 | second:commits=0.0 146 | second:conflicts=0.0 147 | second:conflicts_resolved=0.0 148 | second:loads=42.42 149 | second:stores=0.0 150 | 1 151 | 152 | >>> stop() 153 | -------------------------------------------------------------------------------- /src/ZEO/schema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | This schema describes the configuration of the ZEO storage server 8 | process. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 | 30 | 31 | One or more storages that are provided by the ZEO server. The 32 | section names are used as the storage names, and must be unique 33 | within each ZEO storage server. Traditionally, these names 34 | represent small integers starting at '1'. 35 | 36 | 37 | 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /src/ZEO/scripts/README.txt: -------------------------------------------------------------------------------- 1 | This directory contains a collection of utilities for working with 2 | ZEO. Some are more useful than others. If you install ZODB using 3 | distutils ("python setup.py install"), some of these will be 4 | installed. 5 | 6 | Unless otherwise noted, these scripts are invoked with the name of the 7 | Data.fs file as their only argument. Example: checkbtrees.py data.fs. 8 | 9 | 10 | parsezeolog.py -- parse BLATHER logs from ZEO server 11 | 12 | This script may be obsolete. It has not been tested against the 13 | current log output of the ZEO server. 14 | 15 | Reports on the time and size of transactions committed by a ZEO 16 | server, by inspecting log messages at BLATHER level. 17 | 18 | 19 | 20 | timeout.py -- script to test transaction timeout 21 | 22 | usage: timeout.py address delay [storage-name] 23 | 24 | This script connects to a storage, begins a transaction, calls store() 25 | and tpc_vote(), and then sleeps forever. This should trigger the 26 | transaction timeout feature of the server. 27 | 28 | 29 | zeopack.py -- pack a ZEO server 30 | 31 | The script connects to a server and calls pack() on a specific 32 | storage. See the script for usage details. 33 | 34 | 35 | zeoreplay.py -- experimental script to replay transactions from a ZEO log 36 | 37 | Like parsezeolog.py, this may be obsolete because it was written 38 | against an earlier version of the ZEO server. See the script for 39 | usage details. 40 | 41 | 42 | zeoup.py 43 | 44 | usage: zeoup.py [options] 45 | 46 | The test will connect to a ZEO server, load the root object, and 47 | attempt to update the zeoup counter in the root. It will report 48 | success if it updates to counter or if it gets a ConflictError. A 49 | ConflictError is considered a success, because the client was able to 50 | start a transaction. 51 | 52 | See the script for details about the options. 53 | 54 | 55 | 56 | zeoserverlog.py -- analyze ZEO server log for performance statistics 57 | 58 | See the module docstring for details; there are a large number of 59 | options. New in ZODB3 3.1.4. 60 | 61 | 62 | zeoqueue.py -- report number of clients currently waiting in the ZEO queue 63 | 64 | See the module docstring for details. 65 | -------------------------------------------------------------------------------- /src/ZEO/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /src/ZEO/scripts/parsezeolog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.3 2 | 3 | """Parse the BLATHER logging generated by ZEO2. 4 | 5 | An example of the log format is: 6 | 2002-04-15T13:05:29 BLATHER(-100) ZEO Server storea(3235680, [714], 235339406490168806) ('10.0.26.30', 45514) # NOQA: E501 line too long 7 | """ 8 | 9 | import re 10 | import time 11 | 12 | 13 | rx_time = re.compile(r'(\d\d\d\d-\d\d-\d\d)T(\d\d:\d\d:\d\d)') 14 | 15 | 16 | def parse_time(line): 17 | """Return the time portion of a zLOG line in seconds or None.""" 18 | mo = rx_time.match(line) 19 | if mo is None: 20 | return None 21 | date, time_ = mo.group(1, 2) 22 | date_l = [int(elt) for elt in date.split('-')] 23 | time_l = [int(elt) for elt in time_.split(':')] 24 | return int(time.mktime(date_l + time_l + [0, 0, 0])) 25 | 26 | 27 | rx_meth = re.compile(r"zrpc:\d+ calling (\w+)\((.*)") 28 | 29 | 30 | def parse_method(line): 31 | pass 32 | 33 | 34 | def parse_line(line): 35 | """Parse a log entry and return time, method info, and client.""" 36 | t = parse_time(line) 37 | if t is None: 38 | return None, None 39 | mo = rx_meth.search(line) 40 | if mo is None: 41 | return None, None 42 | meth_name = mo.group(1) 43 | meth_args = mo.group(2).strip() 44 | if meth_args.endswith(')'): 45 | meth_args = meth_args[:-1] 46 | meth_args = [s.strip() for s in meth_args.split(",")] 47 | m = meth_name, tuple(meth_args) 48 | return t, m 49 | 50 | 51 | class TStats: 52 | 53 | counter = 1 54 | 55 | def __init__(self): 56 | self.id = TStats.counter 57 | TStats.counter += 1 58 | 59 | fields = ("time", "vote", "done", "user", "path") 60 | fmt = "%-24s %5s %5s %-15s %s" 61 | hdr = fmt % fields 62 | 63 | def report(self): 64 | """Print a report about the transaction""" 65 | if hasattr(self, "vote"): 66 | d_vote = self.vote - self.begin 67 | else: 68 | d_vote = "*" 69 | if hasattr(self, "finish"): 70 | d_finish = self.finish - self.begin 71 | else: 72 | d_finish = "*" 73 | print(self.fmt % (time.ctime(self.begin), d_vote, d_finish, 74 | self.user, self.url)) 75 | 76 | 77 | class TransactionParser: 78 | 79 | def __init__(self): 80 | self.txns = {} 81 | self.skipped = 0 82 | 83 | def parse(self, line): 84 | t, m = parse_line(line) 85 | if t is None: 86 | return 87 | name = m[0] 88 | meth = getattr(self, name, None) 89 | if meth is not None: 90 | meth(t, m[1]) 91 | 92 | def tpc_begin(self, time, args): 93 | t = TStats() 94 | t.begin = time 95 | t.user = args[1] 96 | t.url = args[2] 97 | t.objects = [] 98 | tid = eval(args[0]) 99 | self.txns[tid] = t 100 | 101 | def get_txn(self, args): 102 | tid = eval(args[0]) 103 | try: 104 | return self.txns[tid] 105 | except KeyError: 106 | print("uknown tid", repr(tid)) 107 | return None 108 | 109 | def tpc_finish(self, time, args): 110 | t = self.get_txn(args) 111 | if t is None: 112 | return 113 | t.finish = time 114 | 115 | def vote(self, time, args): 116 | t = self.get_txn(args) 117 | if t is None: 118 | return 119 | t.vote = time 120 | 121 | def get_txns(self): 122 | L = [(t.id, t) for t in self.txns.values()] 123 | L.sort() 124 | return [t for (id, t) in L] 125 | 126 | 127 | if __name__ == "__main__": 128 | import fileinput 129 | 130 | p = TransactionParser() 131 | i = 0 132 | for line in fileinput.input(): 133 | i += 1 134 | try: 135 | p.parse(line) 136 | except: # NOQA: E722 bare except 137 | print("line", i) 138 | raise 139 | print("Transaction: %d" % len(p.txns)) 140 | print(TStats.hdr) 141 | for txn in p.get_txns(): 142 | txn.report() 143 | -------------------------------------------------------------------------------- /src/ZEO/scripts/tests.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2004 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | import doctest 15 | import re 16 | import unittest 17 | 18 | from zope.testing import renormalizing 19 | 20 | 21 | def test_suite(): 22 | return unittest.TestSuite(( 23 | doctest.DocFileSuite( 24 | 'zeopack.test', 25 | checker=renormalizing.RENormalizing([ 26 | (re.compile('usage: Usage: '), 'Usage: '), # Py 2.4 27 | (re.compile('options:'), 'Options:'), # Py 2.4 28 | ]), 29 | ), 30 | )) 31 | -------------------------------------------------------------------------------- /src/ZEO/scripts/timeout.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.3 2 | 3 | """Transaction timeout test script. 4 | 5 | This script connects to a storage, begins a transaction, calls store() 6 | and tpc_vote(), and then sleeps forever. This should trigger the 7 | transaction timeout feature of the server. 8 | 9 | usage: timeout.py address delay [storage-name] 10 | 11 | """ 12 | 13 | import sys 14 | import time 15 | 16 | from ZODB.Connection import TransactionMetaData 17 | from ZODB.tests.MinPO import MinPO 18 | from ZODB.tests.StorageTestBase import zodb_pickle 19 | 20 | from ZEO.ClientStorage import ClientStorage 21 | 22 | 23 | ZERO = '\0' * 8 24 | 25 | 26 | def main(): 27 | if len(sys.argv) not in (3, 4): 28 | sys.stderr.write("Usage: timeout.py address delay [storage-name]\n" % 29 | sys.argv[0]) 30 | sys.exit(2) 31 | 32 | hostport = sys.argv[1] 33 | delay = float(sys.argv[2]) 34 | if sys.argv[3:]: 35 | name = sys.argv[3] 36 | else: 37 | name = "1" 38 | 39 | if "/" in hostport: 40 | address = hostport 41 | else: 42 | if ":" in hostport: 43 | i = hostport.index(":") 44 | host, port = hostport[:i], hostport[i + 1:] 45 | else: 46 | host, port = "", hostport 47 | port = int(port) 48 | address = (host, port) 49 | 50 | print("Connecting to %s..." % repr(address)) 51 | storage = ClientStorage(address, name) 52 | print("Connected. Now starting a transaction...") 53 | 54 | oid = storage.new_oid() 55 | revid = ZERO 56 | data = MinPO("timeout.py") 57 | pickled_data = zodb_pickle(data) 58 | t = TransactionMetaData() 59 | t.user = "timeout.py" 60 | storage.tpc_begin(t) 61 | storage.store(oid, revid, pickled_data, '', t) 62 | print("Stored. Now voting...") 63 | storage.tpc_vote(t) 64 | 65 | print("Voted; now sleeping %s..." % delay) 66 | time.sleep(delay) 67 | print("Done.") 68 | 69 | 70 | if __name__ == "__main__": 71 | main() 72 | -------------------------------------------------------------------------------- /src/ZEO/scripts/zeoup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.3 2 | 3 | """Make sure a ZEO server is running. 4 | 5 | usage: zeoup.py [options] 6 | 7 | The test will connect to a ZEO server, load the root object, and attempt to 8 | update the zeoup counter in the root. It will report success if it updates 9 | the counter or if it gets a ConflictError. A ConflictError is considered a 10 | success, because the client was able to start a transaction. 11 | 12 | Options: 13 | 14 | -p port -- port to connect to 15 | 16 | -h host -- host to connect to (default is current host) 17 | 18 | -S storage -- storage name (default '1') 19 | 20 | -U path -- Unix-domain socket to connect to 21 | 22 | --nowrite -- Do not update the zeoup counter. 23 | 24 | -1 -- Connect to a ZEO 1.0 server. 25 | 26 | You must specify either -p and -h or -U. 27 | """ 28 | 29 | import getopt 30 | import logging 31 | import socket 32 | import sys 33 | import time 34 | 35 | import transaction 36 | import ZODB 37 | from persistent.mapping import PersistentMapping 38 | from ZODB.POSException import ConflictError 39 | from ZODB.tests.MinPO import MinPO 40 | 41 | from ZEO.ClientStorage import ClientStorage 42 | 43 | 44 | ZEO_VERSION = 2 45 | 46 | 47 | def setup_logging(): 48 | # Set up logging to stderr which will show messages originating 49 | # at severity ERROR or higher. 50 | root = logging.getLogger() 51 | root.setLevel(logging.ERROR) 52 | fmt = logging.Formatter( 53 | "------\n%(asctime)s %(levelname)s %(name)s %(message)s", 54 | "%Y-%m-%dT%H:%M:%S") 55 | handler = logging.StreamHandler() 56 | handler.setFormatter(fmt) 57 | root.addHandler(handler) 58 | 59 | 60 | def check_server(addr, storage, write): 61 | t0 = time.time() 62 | if ZEO_VERSION == 2: 63 | # TODO: should do retries w/ exponential backoff. 64 | cs = ClientStorage(addr, storage=storage, wait=0, 65 | read_only=(not write)) 66 | else: 67 | cs = ClientStorage(addr, storage=storage, debug=1, 68 | wait_for_server_on_startup=1) 69 | # _startup() is an artifact of the way ZEO 1.0 works. The 70 | # ClientStorage doesn't get fully initialized until registerDB() 71 | # is called. The only thing we care about, though, is that 72 | # registerDB() calls _startup(). 73 | 74 | if write: 75 | db = ZODB.DB(cs) 76 | cn = db.open() 77 | root = cn.root() 78 | try: 79 | # We store the data in a special `monitor' dict under the root, 80 | # where other tools may also store such heartbeat and bookkeeping 81 | # type data. 82 | monitor = root.get('monitor') 83 | if monitor is None: 84 | monitor = root['monitor'] = PersistentMapping() 85 | obj = monitor['zeoup'] = monitor.get('zeoup', MinPO(0)) 86 | obj.value += 1 87 | transaction.commit() 88 | except ConflictError: 89 | pass 90 | cn.close() 91 | db.close() 92 | else: 93 | data, serial = cs.load("\0\0\0\0\0\0\0\0", "") 94 | cs.close() 95 | t1 = time.time() 96 | print("Elapsed time: %.2f" % (t1 - t0)) 97 | 98 | 99 | def usage(exit=1): 100 | print(__doc__) 101 | print(" ".join(sys.argv)) 102 | sys.exit(exit) 103 | 104 | 105 | def main(): 106 | host = None 107 | port = None 108 | unix = None 109 | write = 1 110 | storage = '1' 111 | try: 112 | opts, args = getopt.getopt(sys.argv[1:], 'p:h:U:S:1', 113 | ['nowrite']) 114 | for o, a in opts: 115 | if o == '-p': 116 | port = int(a) 117 | elif o == '-h': 118 | host = a 119 | elif o == '-U': 120 | unix = a 121 | elif o == '-S': 122 | storage = a 123 | elif o == '--nowrite': 124 | write = 0 125 | elif o == '-1': 126 | ZEO_VERSION = 1 # NOQA: F841 unused variable 127 | except Exception as err: 128 | s = str(err) 129 | if s: 130 | s = ": " + s 131 | print(err.__class__.__name__ + s) 132 | usage() 133 | 134 | if unix is not None: 135 | addr = unix 136 | else: 137 | if host is None: 138 | host = socket.gethostname() 139 | if port is None: 140 | usage() 141 | addr = host, port 142 | 143 | setup_logging() 144 | check_server(addr, storage, write) 145 | 146 | 147 | if __name__ == "__main__": 148 | try: 149 | main() 150 | except SystemExit: 151 | raise 152 | except Exception as err: 153 | s = str(err) 154 | if s: 155 | s = ": " + s 156 | print(err.__class__.__name__ + s) 157 | sys.exit(1) 158 | -------------------------------------------------------------------------------- /src/ZEO/server.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The full path to an SSL certificate file. 8 | 9 | 10 | 11 | 12 | 13 | The full path to an SSL key file for the server certificate. 14 | 15 | 16 | 17 | 18 | 19 | Dotted name of importable function for retrieving a password 20 | for the client certificate key. 21 | 22 | 23 | 24 | 25 | 26 | Path to a file or directory containing client certificates to 27 | be authenticated. This can also be - or SIGNED to require 28 | signed client certificates. 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | The content of a ZEO section describe operational parameters 40 | of a ZEO server except for the storage(s) to be served. 41 | 42 | 43 | 45 | 46 | The address at which the server should listen. This can be in 47 | the form 'host:port' to signify a TCP/IP connection or a 48 | pathname string to signify a Unix domain socket connection (at 49 | least one '/' is required). A hostname may be a DNS name or a 50 | dotted IP address. If the hostname is omitted, the platform's 51 | default behavior is used when binding the listening socket ('' 52 | is passed to socket.bind() as the hostname portion of the 53 | address). 54 | 55 | 56 | 57 | 60 | 61 | Flag indicating whether the server should operate in read-only 62 | mode. Defaults to false. Note that even if the server is 63 | operating in writable mode, individual storages may still be 64 | read-only. But if the server is in read-only mode, no write 65 | operations are allowed, even if the storages are writable. Note 66 | that pack() is considered a read-only operation. 67 | 68 | 69 | 70 | 73 | 74 | The storage server keeps a queue of the objects modified by the 75 | last N transactions, where N == invalidation_queue_size. This 76 | queue is used to speed client cache verification when a client 77 | disconnects for a short period of time. 78 | 79 | 80 | 81 | 82 | 83 | The maximum age of a client for which quick-verification 84 | invalidations will be provided by iterating over the served 85 | storage. This option should only be used if the served storage 86 | supports efficient iteration from a starting point near the 87 | end of the transaction history (e.g. end of file). 88 | 89 | 90 | 91 | 93 | 94 | The maximum amount of time to wait for a transaction to commit 95 | after acquiring the storage lock, specified in seconds. If the 96 | transaction takes too long, the client connection will be closed 97 | and the transaction aborted. 98 | 99 | 100 | 101 | 103 | 104 | The full path to the file in which to write the ZEO server's Process ID 105 | at startup. If omitted, $INSTANCE/var/ZEO.pid is used. 106 | 107 | $INSTANCE/var/ZEO.pid (or $clienthome/ZEO.pid) 108 | 109 | 110 | 112 | 113 | Flag indicating whether the server should return conflict 114 | errors to the client, for resolution there. 115 | 116 | 117 | 118 | 119 | 120 | Use msgpack to serialize and de-serialize ZEO protocol messages. 121 | 122 | An advantage of using msgpack for ZEO communication is that 123 | it's a tiny bit faster. 124 | 125 | msgpack can also be enabled by setting the ``ZEO_MSGPACK`` 126 | environment to a non-empty string. 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /src/ZEO/shortrepr.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | 15 | REPR_LIMIT = 60 16 | 17 | 18 | def short_repr(obj): 19 | """Return an object repr limited to REPR_LIMIT bytes.""" 20 | 21 | # Some of the objects being repr'd are large strings. A lot of memory 22 | # would be wasted to repr them and then truncate, so they are treated 23 | # specially in this function. 24 | # Also handle short repr of a tuple containing a long string. 25 | 26 | # This strategy works well for arguments to StorageServer methods. 27 | # The oid is usually first and will get included in its entirety. 28 | # The pickle is near the beginning, too, and you can often fit the 29 | # module name in the pickle. 30 | 31 | if isinstance(obj, str): 32 | if len(obj) > REPR_LIMIT: 33 | r = repr(obj[:REPR_LIMIT]) 34 | else: 35 | r = repr(obj) 36 | if len(r) > REPR_LIMIT: 37 | r = r[:REPR_LIMIT - 4] + '...' + r[-1] 38 | return r 39 | elif isinstance(obj, (list, tuple)): 40 | elts = [] 41 | size = 0 42 | for elt in obj: 43 | r = short_repr(elt) 44 | elts.append(r) 45 | size += len(r) 46 | if size > REPR_LIMIT: 47 | break 48 | if isinstance(obj, tuple): 49 | r = "(%s)" % (", ".join(elts)) 50 | else: 51 | r = "[%s]" % (", ".join(elts)) 52 | else: 53 | r = repr(obj) 54 | if len(r) > REPR_LIMIT: 55 | return r[:REPR_LIMIT] + '...' 56 | else: 57 | return r 58 | -------------------------------------------------------------------------------- /src/ZEO/tests/Cache.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """Tests of the ZEO cache""" 15 | 16 | from ZODB.Connection import TransactionMetaData 17 | from ZODB.tests.MinPO import MinPO 18 | from ZODB.tests.StorageTestBase import zodb_unpickle 19 | 20 | 21 | class TransUndoStorageWithCache: 22 | 23 | def checkUndoInvalidation(self): 24 | oid = self._storage.new_oid() 25 | revid = self._dostore(oid, data=MinPO(23)) 26 | revid = self._dostore(oid, revid=revid, data=MinPO(24)) 27 | revid = self._dostore(oid, revid=revid, data=MinPO(25)) 28 | 29 | info = self._storage.undoInfo() 30 | if not info: 31 | # Preserved this comment, but don't understand it: 32 | # "Perhaps we have an old storage implementation that 33 | # does do the negative nonsense." 34 | info = self._storage.undoInfo(0, 20) 35 | tid = info[0]['id'] 36 | 37 | # Now start an undo transaction 38 | t = TransactionMetaData() 39 | t.note('undo1') 40 | oids = self._begin_undos_vote(t, tid) 41 | 42 | # Make sure this doesn't load invalid data into the cache 43 | self._storage.load(oid, '') 44 | 45 | self._storage.tpc_finish(t) 46 | 47 | [uoid] = oids 48 | assert uoid == oid 49 | data, revid = self._storage.load(oid, '') 50 | obj = zodb_unpickle(data) 51 | assert obj == MinPO(24) 52 | -------------------------------------------------------------------------------- /src/ZEO/tests/TestThread.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """A Thread base class for use with unittest.""" 15 | import sys 16 | import threading 17 | 18 | 19 | class TestThread(threading.Thread): 20 | """Base class for defining threads that run from unittest. 21 | 22 | The subclass should define a testrun() method instead of a run() 23 | method. 24 | 25 | Call cleanup() when the test is done with the thread, instead of join(). 26 | If the thread exits with an uncaught exception, it's captured and 27 | re-raised when cleanup() is called. cleanup() should be called by 28 | the main thread! Trying to tell unittest that a test failed from 29 | another thread creates a nightmare of timing-depending cascading 30 | failures and missed errors (tracebacks that show up on the screen, 31 | but don't cause unittest to believe the test failed). 32 | 33 | cleanup() also joins the thread. If the thread ended without raising 34 | an uncaught exception, and the join doesn't succeed in the timeout 35 | period, then the test is made to fail with a "Thread still alive" 36 | message. 37 | """ 38 | 39 | def __init__(self, testcase): 40 | threading.Thread.__init__(self) 41 | # In case this thread hangs, don't stop Python from exiting. 42 | self.daemon = True 43 | self._exc_info = None 44 | self._testcase = testcase 45 | 46 | def run(self): 47 | try: 48 | self.testrun() 49 | except: # NOQA: E722 blank except 50 | self._exc_info = sys.exc_info() 51 | 52 | def cleanup(self, timeout=15): 53 | self.join(timeout) 54 | if self._exc_info: 55 | et, ev, etb = self._exc_info 56 | if ev is None: 57 | ev = et() 58 | raise ev.with_traceback(etb) 59 | if self.is_alive(): 60 | self._testcase.fail("Thread did not finish: %s" % self) 61 | -------------------------------------------------------------------------------- /src/ZEO/tests/ThreadTests.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """Compromising positions involving threads.""" 15 | 16 | import threading 17 | 18 | from ZODB.Connection import TransactionMetaData 19 | from ZODB.tests.StorageTestBase import MinPO 20 | from ZODB.tests.StorageTestBase import zodb_pickle 21 | 22 | import ZEO.Exceptions 23 | 24 | 25 | ZERO = '\0' * 8 26 | 27 | 28 | class BasicThread(threading.Thread): 29 | def __init__(self, storage, doNextEvent, threadStartedEvent): 30 | self.storage = storage 31 | self.trans = TransactionMetaData() 32 | self.doNextEvent = doNextEvent 33 | self.threadStartedEvent = threadStartedEvent 34 | self.gotValueError = 0 35 | self.gotDisconnected = 0 36 | threading.Thread.__init__(self) 37 | self.daemon = True 38 | 39 | def join(self): 40 | threading.Thread.join(self, 10) 41 | assert not self.is_alive() 42 | 43 | 44 | class GetsThroughVoteThread(BasicThread): 45 | # This thread gets partially through a transaction before it turns 46 | # execution over to another thread. We're trying to establish that a 47 | # tpc_finish() after a storage has been closed by another thread will get 48 | # a ClientStorageError error. 49 | # 50 | # This class gets does a tpc_begin(), store(), tpc_vote() and is waiting 51 | # to do the tpc_finish() when the other thread closes the storage. 52 | def run(self): 53 | self.storage.tpc_begin(self.trans) 54 | oid = self.storage.new_oid() 55 | self.storage.store(oid, ZERO, zodb_pickle(MinPO("c")), '', self.trans) 56 | self.storage.tpc_vote(self.trans) 57 | self.threadStartedEvent.set() 58 | self.doNextEvent.wait(10) 59 | try: 60 | self.storage.tpc_finish(self.trans) 61 | except ZEO.Exceptions.ClientStorageError: 62 | self.gotValueError = 1 63 | self.storage.tpc_abort(self.trans) 64 | 65 | 66 | class GetsThroughBeginThread(BasicThread): 67 | # This class is like the above except that it is intended to be run when 68 | # another thread is already in a tpc_begin(). Thus, this thread will 69 | # block in the tpc_begin until another thread closes the storage. When 70 | # that happens, this one will get disconnected too. 71 | def run(self): 72 | try: 73 | self.storage.tpc_begin(self.trans) 74 | except ZEO.Exceptions.ClientStorageError: 75 | self.gotValueError = 1 76 | 77 | 78 | class ThreadTests: 79 | # Thread 1 should start a transaction, but not get all the way through it. 80 | # Main thread should close the connection. Thread 1 should then get 81 | # disconnected. 82 | def checkDisconnectedOnThread2Close(self): 83 | doNextEvent = threading.Event() 84 | threadStartedEvent = threading.Event() 85 | thread1 = GetsThroughVoteThread(self._storage, 86 | doNextEvent, threadStartedEvent) 87 | thread1.start() 88 | threadStartedEvent.wait(10) 89 | self._storage.close() 90 | doNextEvent.set() 91 | thread1.join() 92 | self.assertEqual(thread1.gotValueError, 1) 93 | 94 | # Thread 1 should start a transaction, but not get all the way through 95 | # it. While thread 1 is in the middle of the transaction, a second thread 96 | # should start a transaction, and it will block in the tcp_begin() -- 97 | # because thread 1 has acquired the lock in its tpc_begin(). Now the main 98 | # thread closes the storage and both sub-threads should get disconnected. 99 | def checkSecondBeginFails(self): 100 | doNextEvent = threading.Event() 101 | threadStartedEvent = threading.Event() 102 | thread1 = GetsThroughVoteThread(self._storage, 103 | doNextEvent, threadStartedEvent) 104 | thread2 = GetsThroughBeginThread(self._storage, 105 | doNextEvent, threadStartedEvent) 106 | thread1.start() 107 | threadStartedEvent.wait(1) 108 | thread2.start() 109 | self._storage.close() 110 | doNextEvent.set() 111 | thread1.join() 112 | thread2.join() 113 | self.assertEqual(thread1.gotValueError, 1) 114 | self.assertEqual(thread2.gotValueError, 1) 115 | 116 | # Run a bunch of threads doing small and large stores in parallel 117 | def checkMTStores(self): 118 | threads = [] 119 | for i in range(5): 120 | t = threading.Thread(target=self.mtstorehelper) 121 | threads.append(t) 122 | t.start() 123 | for t in threads: 124 | t.join(30) 125 | for i in threads: 126 | self.assertFalse(t.is_alive()) 127 | 128 | # Helper for checkMTStores 129 | def mtstorehelper(self): 130 | objs = [] 131 | for i in range(10): 132 | objs.append(MinPO("X" * 200000)) 133 | objs.append(MinPO("X")) 134 | for obj in objs: 135 | self._dostore(data=obj) 136 | -------------------------------------------------------------------------------- /src/ZEO/tests/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | -------------------------------------------------------------------------------- /src/ZEO/tests/client-config.test: -------------------------------------------------------------------------------- 1 | ZEO Client Configuration 2 | ======================== 3 | 4 | Here we'll describe (and test) the various ZEO Client configuration 5 | options. To facilitate this, we'l start a server that our client can 6 | connect to: 7 | 8 | >>> addr, _ = start_server(blob_dir='server-blobs') 9 | 10 | The simplest client configuration specified a server address: 11 | 12 | >>> import ZODB.config 13 | >>> storage = ZODB.config.storageFromString(""" 14 | ... 15 | ... server %s:%s 16 | ... 17 | ... """ % addr) 18 | 19 | >>> storage.getName(), storage.__class__.__name__ 20 | ... # doctest: +ELLIPSIS 21 | ("[('127.0.0.1', ...)] (connected)", 'ClientStorage') 22 | 23 | >>> storage.blob_dir 24 | >>> storage._storage 25 | '1' 26 | >>> storage._cache.maxsize 27 | 20971520 28 | >>> storage._cache.path 29 | >>> storage._is_read_only 30 | False 31 | >>> storage._read_only_fallback 32 | False 33 | >>> storage._blob_cache_size 34 | 35 | >>> storage.close() 36 | 37 | >>> storage = ZODB.config.storageFromString(""" 38 | ... 39 | ... server %s:%s 40 | ... blob-dir blobs 41 | ... storage 2 42 | ... cache-size 100 43 | ... name bob 44 | ... client cache 45 | ... read-only true 46 | ... drop-cache-rather-verify true 47 | ... blob-cache-size 1000MB 48 | ... blob-cache-size-check 10 49 | ... wait false 50 | ... 51 | ... """ % addr) 52 | 53 | 54 | >>> storage.getName(), storage.__class__.__name__ 55 | ('bob (disconnected)', 'ClientStorage') 56 | 57 | >>> storage.blob_dir 58 | 'blobs' 59 | >>> storage._storage 60 | '2' 61 | >>> storage._cache.maxsize 62 | 100 63 | >>> import os 64 | >>> storage._cache.path == os.path.abspath('cache-2.zec') 65 | True 66 | 67 | >>> storage._is_read_only 68 | True 69 | >>> storage._read_only_fallback 70 | False 71 | >>> storage._blob_cache_size 72 | 1048576000 73 | 74 | >>> print(storage._blob_cache_size_check) 75 | 104857600 76 | 77 | In isolated runs, the ``close`` below failed with a timeout. 78 | Set up a log handler to get at the associated log message. 79 | Note that we are interested only in this specific log message 80 | (from `ZEO.asyncio.client.ClientIO.close_co`). 81 | Occasionally (when the server response for the access to the 82 | non existing storage `2` is processed sufficiently fast), 83 | there will we other error log entries; we ignore them. 84 | 85 | Note: ``InstalledHandler`` changes the logger's level. 86 | Therefore, we cannot set ``level`` to ``ERROR`` even though 87 | we are only interested in error messages for this test (it would discard 88 | other log messages important for the timeout analysis). 89 | Instead, we initially request the ``DEBUG`` level 90 | and later set the handler's level to ``ERROR``. 91 | The main logfile might get unusual entries as a side effect. 92 | 93 | >>> import logging 94 | >>> from zope.testing.loggingsupport import InstalledHandler 95 | >>> handler = InstalledHandler("ZEO.asyncio.client", level=logging.DEBUG) 96 | >>> handler.setLevel(logging.ERROR) 97 | 98 | >>> storage.close() 99 | 100 | Check log records 101 | 102 | >>> def check_log_records(records): 103 | ... records = [r.message for r in records] 104 | ... return any("as a bug" in r for r in records) and records or [] 105 | >>> check_log_records(handler.records) 106 | [] 107 | 108 | Cleanup 109 | 110 | >>> handler.uninstall() 111 | -------------------------------------------------------------------------------- /src/ZEO/tests/client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYjCCAkqgAwIBAgIJAIleRCrCxQqfMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwIBcNMTkwNDI2MDg1ODA5WhgPMzAwNDEyMTgwODU4MDla 5 | MEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ 6 | bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 7 | ggEKAoIBAQD0rlGmUNcmllyuQ/7YCNozFc5nUA1ENjvsF1S4DG0qhkhPQxz2RQ2v 8 | hO8tpvgItVzmLDAR+KvvE9I8R7GRmXzwgm6qNZLrZ3XUkWVtTrZWrnhlwRiKKae5 9 | HWHnN5TMoao1wwTFtOjuNyBKPcWcfjAJR5ZY5QRVqyelp8vU7hWJP0G5rFZcoPuC 10 | egVLODpBuXMI7Z/GSepp0t2jBOafIXkRSLa59ozJjjtqRkULwF/Twaetm0CwQ0VE 11 | dORl/vpg2SK+834ad+vrSS2IvkewJqsCSNdiql+LxinCuUWSqe5yq/dXNDA5Cy+7 12 | 3hQlJy6IPlX0wRzafeWmT4itB5oM2YLpAgMBAAGjUzBRMB0GA1UdDgQWBBSH6QYi 13 | Dj6xPq3C4dirKpgTN3N/wTAfBgNVHSMEGDAWgBSH6QYiDj6xPq3C4dirKpgTN3N/ 14 | wTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQDneWDUrFmQlCNy 15 | luXVrld4qi+ufpcjl7r/fsA7cTqNN3IWtzCbvbqQ8X2pSjfFkjvdcyqJAZ56Db5Q 16 | sb8F8m9zuTEm3qUlAqkbQb6vhiwwbEjeflPRkx/MyuUvForGgvnKQAB74bApTHo4 17 | u3DTuXN5px6Cc4LgcDHZXRPIr/8v+ZzBjL+csp6HO5ezl7qa5xc9hyXkGm6m/2iE 18 | aosKD1ll3XaBUfR7qyL5T46ccjdmr+w22HXuUQvhpQ4TGBIp3xEn1DDAClfjqH6A 19 | PjIfiwjYyg9PHblx8gmjGyCUp4CgR0pEbetolemdPLm+t7cSB3VNbzKnV04G9x73 20 | zieSaJid 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /src/ZEO/tests/client_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA9K5RplDXJpZcrkP+2AjaMxXOZ1ANRDY77BdUuAxtKoZIT0Mc 3 | 9kUNr4TvLab4CLVc5iwwEfir7xPSPEexkZl88IJuqjWS62d11JFlbU62Vq54ZcEY 4 | iimnuR1h5zeUzKGqNcMExbTo7jcgSj3FnH4wCUeWWOUEVasnpafL1O4ViT9BuaxW 5 | XKD7gnoFSzg6QblzCO2fxknqadLdowTmnyF5EUi2ufaMyY47akZFC8Bf08GnrZtA 6 | sENFRHTkZf76YNkivvN+Gnfr60ktiL5HsCarAkjXYqpfi8YpwrlFkqnucqv3VzQw 7 | OQsvu94UJScuiD5V9MEc2n3lpk+IrQeaDNmC6QIDAQABAoIBAErM27MvdYabYvv3 8 | V3otwp7pZK8avuOCfPEg9MpLKjhc0tBAYSM8WwG0bvYS3DK1VxAapBtqXQ16jsPU 9 | 2wj61kIkbbZlKGQEvfXc+Rfgf0eikLXywRDDyT2DKQHpcPjZ11IWK2hRdQAWJC3u 10 | EnJT9VVw6BqG8LtL1pQC5wJSQo0xC1sJ/MTr/szLvKRjuYZE7YStpUfV6RYq2KQF 11 | 7Oa9nPKtxlIbDCa7z4S6y5yiusYrSSFilK0pVSU+9789kGNZMLzKbnGu+YSVB/Bx 12 | MLXWRAD8DV9zign255pIU/xI5VKjOwID38JfgdcebV/KeCPu8W6jKKbfUsUCqcjL 13 | YjDtHYECgYEA/SaxUoejMOasHppnsAewy/I+DzMuX+KYztqAnzjsuGwRxmxjYyQe 14 | w7EidinM3WuloJIBZzA9aULmWjSKOfTsuGm+Mokucbbw9jaWVT6Co3kWrHySInhZ 15 | sfTwHKz5ojGBcQD4l06xaVM9utNi6r8wvJijFl5xIsMzc5szEkWs9vkCgYEA9285 16 | bGSAAwzUFHVk1pyLKozM2gOtF5rrAUQlWtNVU6K2tw+MKEGara0f+HFZrJZC9Rh2 17 | HBm2U9PPt/kJ73HErQG+E6n0jfol8TQ3ZKz3tlSxImh0CiaKLnh4ahf7o8zU16nT 18 | XDfu3+Rf11EhORXYfZLmdubfCOD4ZaB2/405N3ECgYEA7b4k0gkoLYi1JJiFwD+4 19 | vhBmUAgVCV/ZeoqiOOZRCnITz3GDdVw6uDXm02o2R8wM5Fu6jZo0UmLNyvGEzyFC 20 | H37PbM6Am7LfYZuqW6w1LClQLfVfmJfGROZvib65QqWTlvj+fbsdyniuhIJ5Z1Tf 21 | BH+kyiEvxyHjdDLRJ9vfsKECgYA8P9MFt7sMAxWpHaS+NUQVyk8fTwHY25oZptRJ 22 | t2fxg49mJ90C+GaHn75HKqKhSb1oHNq1oPUqmEreC0AGE/fGAMSd2SZ5Y83VW9eZ 23 | JhzzQtAXBsQqrJO9GQyJGOnnSrsRAIM800nRLrS/ozupwM4EVb7UeQcaDF2vsVEI 24 | jQS/oQKBgHj26xn7AunX5GS8EYe4GPj4VZehmlnEKONGrPrr25aWkaY4kDJgMLUb 25 | AxwIQHbCMm5TMqIxi5l39/O9dxuuGCkOs37j7C3f3VVFuQW1KKyHem9OClgFDZj3 26 | tEEk1N3NevrH06NlmAHweHMuJXL8mBvM375zH9tSw5mgG0OMRbnG 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/ZEO/tests/component.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ZEO/tests/drop_cache_rather_than_verify.txt: -------------------------------------------------------------------------------- 1 | Avoiding cache verifification 2 | ============================= 3 | 4 | For large databases it is common to also use very large ZEO cache 5 | files. If a client has beed disconnected for too long, the server 6 | can't play back missing invalidations. In this case, the cache is 7 | cleared. When this happens, a ZEO.interfaces.StaleCache event is 8 | published, largely for backward compatibility. 9 | 10 | ClientStorage used to provide an option to drop it's cache rather than 11 | doing verification. This is now the only behavior. Cache 12 | verification is no longer supported. 13 | 14 | - Invalidates all object caches 15 | 16 | - Drops or clears it's client cache. (The end result is that the cache 17 | is working but empty.) 18 | 19 | - Logs a CRITICAL message. 20 | 21 | Here's an example that shows that this is actually what happens. 22 | 23 | Start a server, create a client to it and commit some data 24 | 25 | >>> addr, admin = start_server(keep=1) 26 | >>> import ZEO, transaction 27 | >>> db = ZEO.DB(addr, client='cache', name='test') 28 | >>> wait_connected(db.storage) 29 | >>> conn = db.open() 30 | >>> conn.root()[1] = conn.root().__class__() 31 | >>> conn.root()[1].x = 1 32 | >>> transaction.commit() 33 | >>> len(db.storage._cache) 34 | 3 35 | 36 | Now, we'll stop the server and restart with a different address: 37 | 38 | >>> stop_server(admin) 39 | >>> addr2, admin = start_server(keep=1) 40 | 41 | And create another client and write some data to it: 42 | 43 | >>> db2 = ZEO.DB(addr2) 44 | >>> wait_connected(db2.storage) 45 | >>> conn2 = db2.open() 46 | >>> for i in range(5): 47 | ... conn2.root()[1].x += 1 48 | ... transaction.commit() 49 | >>> db2.close() 50 | >>> stop_server(admin) 51 | 52 | Now, we'll restart the server. Before we do that, we'll capture 53 | logging and event data: 54 | 55 | >>> import logging, zope.testing.loggingsupport, ZODB.event 56 | >>> handler = zope.testing.loggingsupport.InstalledHandler( 57 | ... 'ZEO', level=logging.ERROR) 58 | >>> events = [] 59 | >>> def event_handler(e): 60 | ... if hasattr(e, 'storage'): 61 | ... events.append(( 62 | ... len(e.storage._cache), str(handler), e.__class__.__name__)) 63 | 64 | >>> old_notify = ZODB.event.notify 65 | >>> ZODB.event.notify = event_handler 66 | 67 | Note that the event handler is saving away the length of the cache and 68 | the state of the log handler. We'll use this to show that the event 69 | is generated before the cache is dropped or the message is logged. 70 | 71 | Now, we'll restart the server on the original address: 72 | 73 | >>> _, admin = start_server(zeo_conf=dict(invalidation_queue_size=1), 74 | ... addr=addr, keep=1) 75 | 76 | >>> wait_connected(db.storage) 77 | 78 | Now, let's verify our assertions above: 79 | 80 | - Publishes a stale-cache event. 81 | 82 | >>> for e in events: 83 | ... print(e) 84 | (3, '', 'StaleCache') 85 | 86 | Note that the length of the cache when the event handler was 87 | called waa non-zero. This is because the cache wasn't cleared 88 | yet. Similarly, the dropping-cache message hasn't been logged 89 | yet. 90 | 91 | >>> del events[:] 92 | 93 | - Drops or clears it's client cache. (The end result is that the cache 94 | is working but empty.) 95 | 96 | >>> len(db.storage._cache) 97 | 0 98 | 99 | - Invalidates all object caches 100 | 101 | >>> transaction.abort() 102 | >>> conn.root()._p_changed 103 | 104 | - Logs a CRITICAL message. 105 | 106 | >>> print(handler) # doctest: +ELLIPSIS 107 | ZEO... CRITICAL 108 | test dropping stale cache 109 | 110 | >>> handler.clear() 111 | 112 | If we access the root object, it'll be loaded from the server: 113 | 114 | >>> conn.root()[1].x 115 | 6 116 | 117 | Similarly, if we simply disconnect the client, and write data from 118 | another client: 119 | 120 | >>> db.close() 121 | 122 | >>> db2 = ZEO.DB(addr) 123 | >>> wait_connected(db2.storage) 124 | >>> conn2 = db2.open() 125 | >>> for i in range(5): 126 | ... conn2.root()[1].x += 1 127 | ... transaction.commit() 128 | >>> db2.close() 129 | 130 | >>> db = ZEO.DB(addr, drop_cache_rather_verify=True, client='cache', 131 | ... name='test') 132 | >>> wait_connected(db.storage) 133 | 134 | 135 | - Drops or clears it's client cache. (The end result is that the cache 136 | is working but empty.) 137 | 138 | >>> len(db.storage._cache) <= 1 139 | True 140 | 141 | (When a database is created, it checks to make sure the root object is 142 | in the database, which is why we get 1, rather than 0 objects in the cache.) 143 | 144 | - Publishes a stale-cache event. 145 | 146 | >>> for e in events: 147 | ... print(e) 148 | (2, '', 'StaleCache') 149 | 150 | >>> del events[:] 151 | 152 | - Logs a CRITICAL message. 153 | 154 | >>> print(handler) # doctest: +ELLIPSIS 155 | ZEO... CRITICAL 156 | test dropping stale cache 157 | 158 | >>> handler.clear() 159 | 160 | If we access the root object, it'll be loaded from the server: 161 | 162 | >>> conn = db.open() 163 | >>> conn.root()[1].x 164 | 11 165 | 166 | .. Cleanup 167 | 168 | >>> db.close() 169 | >>> handler.uninstall() 170 | >>> ZODB.event.notify = old_notify 171 | -------------------------------------------------------------------------------- /src/ZEO/tests/dynamic_server_ports.test: -------------------------------------------------------------------------------- 1 | The storage server can be told to bind to port 0, allowing the OS to 2 | pick a port dynamically. For this to be useful, there needs to be a 3 | way to tell someone. For this reason, the server posts events to 4 | ZODB.notify. 5 | 6 | >>> import ZODB.event 7 | >>> old_notify = ZODB.event.notify 8 | 9 | >>> last_event = None 10 | >>> def notify(event): 11 | ... global last_event 12 | ... last_event = event 13 | >>> ZODB.event.notify = notify 14 | 15 | Now, let's start a server and verify that we get a serving event: 16 | 17 | >>> import ZEO 18 | >>> addr, stop = ZEO.server() 19 | 20 | >>> isinstance(last_event, ZEO.StorageServer.Serving) 21 | True 22 | 23 | >>> last_event.address == addr 24 | True 25 | 26 | >>> server = last_event.server 27 | >>> server.addr == addr 28 | True 29 | 30 | Let's make sure we can connect. 31 | 32 | >>> client = ZEO.client(last_event.address).close() 33 | 34 | If we close the server, we'll get a closed event: 35 | 36 | >>> stop() 37 | >>> isinstance(last_event, ZEO.StorageServer.Closed) 38 | True 39 | >>> last_event.server is server 40 | True 41 | 42 | If we pass an empty string as the host part of the server address, we 43 | can't really assign a single address, so the server addr attribute is 44 | left alone: 45 | 46 | >>> addr, stop = ZEO.server(port=('', 0)) 47 | >>> isinstance(last_event, ZEO.StorageServer.Serving) 48 | True 49 | 50 | >>> last_event.address[1] > 0 51 | True 52 | 53 | >>> last_event.server.addr 54 | ('', 0) 55 | 56 | >>> stop() 57 | 58 | The runzeo module provides some process support, including getting the 59 | server configuration via a ZConfig configuration file. To spell a 60 | dynamic port using ZConfig, you'd use a hostname by itself. In this 61 | case, ZConfig passes None as the port. 62 | 63 | >>> import ZEO.runzeo 64 | >>> with open('conf', 'w') as f: 65 | ... _ = f.write(""" 66 | ... 67 | ... address 127.0.0.1 68 | ... 69 | ... 70 | ... 71 | ... """) 72 | >>> options = ZEO.runzeo.ZEOOptions() 73 | >>> options.realize('-C conf'.split()) 74 | >>> options.address 75 | ('127.0.0.1', None) 76 | 77 | >>> rs = ZEO.runzeo.ZEOServer(options) 78 | >>> rs.check_socket() 79 | >>> options.address 80 | ('127.0.0.1', 0) 81 | 82 | 83 | .. cleanup 84 | 85 | >>> ZODB.event.notify = old_notify 86 | -------------------------------------------------------------------------------- /src/ZEO/tests/invalidation-age.txt: -------------------------------------------------------------------------------- 1 | Invalidation age 2 | ================ 3 | 4 | When a ZEO client with a non-empty cache connects to the server, it 5 | needs to verify whether the data in its cache is current. It does 6 | this in one of 2 ways: 7 | 8 | quick verification 9 | It gets a list of invalidations from the server since the last 10 | transaction the client has seen and applies those to it's disk and 11 | in-memory caches. This is only possible if there haven't been too 12 | many transactions since the client was last connected. 13 | 14 | full verification 15 | If quick verification isn't possible, the client iterates through 16 | it's disk cache asking the server to verify whether each current 17 | entry is valid. 18 | 19 | Unfortunately, for large caches, full verification is soooooo not 20 | quick that it is impractical. Quick verificatioin is highly 21 | desireable. 22 | 23 | To support quick verification, the server keeps a list of recent 24 | invalidations. The size of this list is controlled by the 25 | invalidation_queue_size parameter. If there is a lot of database 26 | activity, the size might need to be quite large to support having 27 | clients be disconnected for more than a few minutes. A very large 28 | invalidation queue size can use a lot of memory. 29 | 30 | To suppliment the invalidation queue, you can also specify an 31 | invalidation_age parameter. When a client connects and presents the 32 | last transaction id it has seen, we first check to see if the 33 | invalidation queue has that transaction id. It it does, then we send 34 | all transactions since that id. Otherwise, we check to see if the 35 | difference between storage's last transaction id and the given id is 36 | less than or equal to the invalidation age. If it is, then we iterate 37 | over the storage, starting with the given id, to get the invalidations 38 | since the given id. 39 | 40 | NOTE: This assumes that iterating from a point near the "end" of a 41 | database is inexpensive. Don't use this option for a storage for which 42 | that is not the case. 43 | 44 | Here's an example. We set up a server, using an 45 | invalidation-queue-size of 5: 46 | 47 | >>> addr, admin = start_server(zeo_conf=dict(invalidation_queue_size=5), 48 | ... keep=True) 49 | 50 | Now, we'll open a client with a persistent cache, set up some data, 51 | and then close client: 52 | 53 | >>> import ZEO, transaction 54 | >>> db = ZEO.DB(addr, client='test') 55 | >>> conn = db.open() 56 | >>> for i in range(9): 57 | ... conn.root()[i] = conn.root().__class__() 58 | ... conn.root()[i].x = 0 59 | >>> transaction.commit() 60 | >>> db.close() 61 | 62 | We'll open another client, and commit some transactions: 63 | 64 | >>> db = ZEO.DB(addr) 65 | >>> conn = db.open() 66 | >>> import transaction 67 | >>> for i in range(2): 68 | ... conn.root()[i].x = 1 69 | ... transaction.commit() 70 | >>> db.close() 71 | 72 | If we reopen the first client, we'll do quick verification. 73 | 74 | >>> db = ZEO.DB(addr, client='test') # doctest: +ELLIPSIS 75 | >>> db._storage._server.client.verify_result 76 | 'quick verification' 77 | 78 | >>> [v.x for v in db.open().root().values()] 79 | [1, 1, 0, 0, 0, 0, 0, 0, 0] 80 | 81 | Now, if we disconnect and commit more than 5 transactions, we'll see 82 | that we had to clear the cache: 83 | 84 | >>> db.close() 85 | >>> db = ZEO.DB(addr) 86 | >>> conn = db.open() 87 | >>> import transaction 88 | >>> for i in range(9): 89 | ... conn.root()[i].x = 2 90 | ... transaction.commit() 91 | >>> db.close() 92 | 93 | >>> db = ZEO.DB(addr, client='test') 94 | >>> db._storage._server.client.verify_result 95 | 'cache too old, clearing' 96 | 97 | >>> [v.x for v in db.open().root().values()] 98 | [2, 2, 2, 2, 2, 2, 2, 2, 2] 99 | 100 | >>> db.close() 101 | 102 | But if we restart the server with invalidation-age set, we can 103 | do quick verification: 104 | 105 | >>> stop_server(admin) 106 | >>> addr, admin = start_server(zeo_conf=dict(invalidation_queue_size=5, 107 | ... invalidation_age=100)) 108 | >>> db = ZEO.DB(addr) 109 | >>> conn = db.open() 110 | >>> import transaction 111 | >>> for i in range(9): 112 | ... conn.root()[i].x = 3 113 | ... transaction.commit() 114 | >>> db.close() 115 | 116 | 117 | >>> db = ZEO.DB(addr, client='test') # doctest: +ELLIPSIS 118 | >>> db._storage._server.client.verify_result 119 | 'quick verification' 120 | 121 | >>> [v.x for v in db.open().root().values()] 122 | [3, 3, 3, 3, 3, 3, 3, 3, 3] 123 | 124 | >>> db.close() 125 | -------------------------------------------------------------------------------- /src/ZEO/tests/new_addr.test: -------------------------------------------------------------------------------- 1 | You can change the address(es) of a client storaage. 2 | 3 | We'll start by setting up a server and connecting to it: 4 | 5 | >>> import ZEO, transaction 6 | 7 | >>> addr, stop = ZEO.server(path='test.fs', threaded=False) 8 | >>> conn = ZEO.connection(addr) 9 | >>> client = conn.db().storage 10 | >>> client.is_connected() 11 | True 12 | >>> conn.root() 13 | {} 14 | >>> conn.root.x = 1 15 | >>> transaction.commit() 16 | 17 | Now we'll close the server: 18 | 19 | >>> stop() 20 | 21 | And wait for the connectin to notice it's disconnected: 22 | 23 | >>> wait_until(lambda : not client.is_connected()) 24 | 25 | Now, we'll restart the server: 26 | 27 | >>> addr, stop = ZEO.server(path='test.fs', threaded=False) 28 | 29 | Update with another client: 30 | 31 | >>> conn2 = ZEO.connection(addr) 32 | >>> conn2.root.x += 1 33 | >>> transaction.commit() 34 | 35 | Update the connection and wait for connect: 36 | 37 | >>> client.new_addr(addr) 38 | >>> wait_until(lambda : client.is_connected()) 39 | >>> _ = transaction.begin() 40 | >>> conn.root() 41 | {'x': 2} 42 | 43 | .. cleanup 44 | 45 | >>> conn.close() 46 | >>> conn2.close() 47 | >>> stop() 48 | -------------------------------------------------------------------------------- /src/ZEO/tests/protocols.test: -------------------------------------------------------------------------------- 1 | Test that old protocols are not supported 2 | ========================================= 3 | 4 | ZEO5 client used to support both ZEO5 and ZEO4 servers. Later support for 5 | ZEO5.client-ZEO4.server interoperability was found to be subject to data 6 | corruptions and removed. Here we verify that current ZEO client rejects 7 | connecting to old ZEO server. 8 | 9 | Let's start a Z4 server 10 | 11 | >>> storage_conf = ''' 12 | ... 13 | ... path Data.fs 14 | ... 15 | ... ''' 16 | 17 | >>> addr, stop = start_server( 18 | ... storage_conf, {'msgpack': 'false'}, protocol=b'4') 19 | 20 | A current client should not be able to connect to an old server: 21 | 22 | >>> import ZEO, logging, zope.testing.loggingsupport 23 | >>> hlog = zope.testing.loggingsupport.InstalledHandler( 24 | ... 'ZEO.asyncio.client', level=logging.ERROR) 25 | >>> logging.getLogger('ZEO.asyncio.client').addHandler(hlog) 26 | >>> ZEO.client(addr, client='client', wait_timeout=2) 27 | Traceback (most recent call last): 28 | ... 29 | ClientDisconnected: timed out waiting for connection 30 | 31 | >>> print(hlog) 32 | ZEO.asyncio.client ERROR 33 | Registration or cache validation failed, b'Z4' 34 | 35 | >>> hlog.uninstall() 36 | -------------------------------------------------------------------------------- /src/ZEO/tests/racetest.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2022 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """Module racetest provides infrastructure for tests that catch concurrency 15 | problems in ZEO. 16 | 17 | It complements ZODB.tests.racetest . 18 | """ 19 | 20 | import time 21 | 22 | import zope.interface 23 | 24 | 25 | # LoadDelayedStorage wraps base storage in injects delays after load* 26 | # operations. 27 | # 28 | # It is useful to catch concurrency problems due to races in between load and 29 | # other storage events, e.g. invalidations. 30 | # 31 | # See e.g. https://github.com/zopefoundation/ZEO/issues/209 for example where 32 | # injecting such delays was effective to catch bugs that were otherwise hard to 33 | # reproduce. 34 | class LoadDelayedStorage: 35 | 36 | def __init__(self, base, tdelay=0.01): 37 | self.base = base 38 | self.tdelay = tdelay 39 | zope.interface.directlyProvides(self, zope.interface.providedBy(base)) 40 | 41 | def _delay(self): 42 | time.sleep(self.tdelay) 43 | 44 | def __getattr__(self, name): 45 | return getattr(self.base, name) 46 | 47 | def __len__(self): # ^^^ __getattr__ does not forward __ 48 | return len(self.base) # because they are mangled 49 | 50 | def load(self, *argv, **kw): 51 | _ = self.base.load(*argv, **kw) 52 | self._delay() 53 | return _ 54 | 55 | def loadBefore(self, *argv, **kw): 56 | _ = self.base.loadBefore(*argv, **kw) 57 | self._delay() 58 | return _ 59 | 60 | def loadSerial(self, *argv, **kw): 61 | _ = self.base.loadSerial(*argv, **kw) 62 | self._delay() 63 | return _ 64 | 65 | 66 | class ZConfigLoadDelayed: 67 | 68 | _factory = LoadDelayedStorage 69 | 70 | def __init__(self, config): 71 | self.config = config 72 | self.name = config.getSectionName() 73 | 74 | def open(self): 75 | base = self.config.base.open() 76 | return self._factory(base) 77 | -------------------------------------------------------------------------------- /src/ZEO/tests/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDsjCCApqgAwIBAgIJAMV1O1+aKDW9MA0GCSqGSIb3DQEBBQUAMG0xCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJWQTENMAsGA1UECgwEWk9EQjERMA8GA1UECwwIem9k 4 | Yi5vcmcxETAPBgNVBAMMCHpvZGIub3JnMRwwGgYJKoZIhvcNAQkBFg16b2RiQHpv 5 | ZGIub3JnMCAXDTE5MDQyNjA5MDEzNloYDzMwMDQxMjE4MDkwMTM2WjBtMQswCQYD 6 | VQQGEwJVUzELMAkGA1UECAwCVkExDTALBgNVBAoMBFpPREIxETAPBgNVBAsMCHpv 7 | ZGIub3JnMREwDwYDVQQDDAh6b2RiLm9yZzEcMBoGCSqGSIb3DQEJARYNem9kYkB6 8 | b2RiLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPSuUaZQ1yaW 9 | XK5D/tgI2jMVzmdQDUQ2O+wXVLgMbSqGSE9DHPZFDa+E7y2m+Ai1XOYsMBH4q+8T 10 | 0jxHsZGZfPCCbqo1kutnddSRZW1OtlaueGXBGIopp7kdYec3lMyhqjXDBMW06O43 11 | IEo9xZx+MAlHlljlBFWrJ6Wny9TuFYk/QbmsVlyg+4J6BUs4OkG5cwjtn8ZJ6mnS 12 | 3aME5p8heRFItrn2jMmOO2pGRQvAX9PBp62bQLBDRUR05GX++mDZIr7zfhp36+tJ 13 | LYi+R7AmqwJI12KqX4vGKcK5RZKp7nKr91c0MDkLL7veFCUnLog+VfTBHNp95aZP 14 | iK0HmgzZgukCAwEAAaNTMFEwHQYDVR0OBBYEFIfpBiIOPrE+rcLh2KsqmBM3c3/B 15 | MB8GA1UdIwQYMBaAFIfpBiIOPrE+rcLh2KsqmBM3c3/BMA8GA1UdEwEB/wQFMAMB 16 | Af8wDQYJKoZIhvcNAQEFBQADggEBAGX342qkGd1wR8C0R+o8Dq1Pkq4zBk+eMdkx 17 | C6wZsxkSoRqsFHAoZSxYNXT3xPGwbl/SO8+NzZO9WNRkQukLYUWe4SjdVtLkYJD8 18 | KcFVbDY2dv/CJRxUftNYTDJC2PscNjgaNRdLpyScprqxFx6IwfhepquIjuQj/7s5 19 | 4txou3D+KKvmNMoFdW1dvrsxn38vkhfFgTi5/FczlDL8n3dmLY3w9bOZAz1BTlVh 20 | FAE9mWJpnxRCVGzGOxaMNA1O8CrUfS4uzoUnWRR0uuftrA4VJOOEVOzHHwdDskso 21 | kswYHeWWJfqLUJlf1Sr+6dQ11VONm86+xY/2Xw0lS+zqXAll/x8= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /src/ZEO/tests/server.pem.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICsjCCAZoCAQAwbTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMQ0wCwYDVQQK 3 | DARaT0RCMREwDwYDVQQLDAh6b2RiLm9yZzERMA8GA1UEAwwIem9kYi5vcmcxHDAa 4 | BgkqhkiG9w0BCQEWDXpvZGJAem9kYi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IB 5 | DwAwggEKAoIBAQD0rlGmUNcmllyuQ/7YCNozFc5nUA1ENjvsF1S4DG0qhkhPQxz2 6 | RQ2vhO8tpvgItVzmLDAR+KvvE9I8R7GRmXzwgm6qNZLrZ3XUkWVtTrZWrnhlwRiK 7 | Kae5HWHnN5TMoao1wwTFtOjuNyBKPcWcfjAJR5ZY5QRVqyelp8vU7hWJP0G5rFZc 8 | oPuCegVLODpBuXMI7Z/GSepp0t2jBOafIXkRSLa59ozJjjtqRkULwF/Twaetm0Cw 9 | Q0VEdORl/vpg2SK+834ad+vrSS2IvkewJqsCSNdiql+LxinCuUWSqe5yq/dXNDA5 10 | Cy+73hQlJy6IPlX0wRzafeWmT4itB5oM2YLpAgMBAAGgADANBgkqhkiG9w0BAQsF 11 | AAOCAQEAVzxIqDiv3evn3LsKrE0HcSkWKnValZz0e4iF96qmstLs2NJa+WsV7p/J 12 | Tg8DgUbQ72G9wN10OQld1k06KUd1SEWhOonBX60lGOkqyn6LHassItbwgCmHC5hk 13 | qs7h0b56s/gSnxYvN3tAWiRzNxdudFQBB7Ughy2SRN3ChsNDBuRIsJQN2yZtYjXM 14 | lZb2J7hZChFGD+L/9Cq6oPhUD+l1aFUv8PvU3jInf/IYyvNQJ3qeYRpOcNR4cnyf 15 | 6oRJn2b3ypFF/4F4ZiOb6Qocpcg7qBRRqztr4C2MZuDST4/zIBAHfKlUwD1/uo7A 16 | BdXUUeM1J1Gaf8GRLSvB8AeZg6/ztA== 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /src/ZEO/tests/server_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA9K5RplDXJpZcrkP+2AjaMxXOZ1ANRDY77BdUuAxtKoZIT0Mc 3 | 9kUNr4TvLab4CLVc5iwwEfir7xPSPEexkZl88IJuqjWS62d11JFlbU62Vq54ZcEY 4 | iimnuR1h5zeUzKGqNcMExbTo7jcgSj3FnH4wCUeWWOUEVasnpafL1O4ViT9BuaxW 5 | XKD7gnoFSzg6QblzCO2fxknqadLdowTmnyF5EUi2ufaMyY47akZFC8Bf08GnrZtA 6 | sENFRHTkZf76YNkivvN+Gnfr60ktiL5HsCarAkjXYqpfi8YpwrlFkqnucqv3VzQw 7 | OQsvu94UJScuiD5V9MEc2n3lpk+IrQeaDNmC6QIDAQABAoIBAErM27MvdYabYvv3 8 | V3otwp7pZK8avuOCfPEg9MpLKjhc0tBAYSM8WwG0bvYS3DK1VxAapBtqXQ16jsPU 9 | 2wj61kIkbbZlKGQEvfXc+Rfgf0eikLXywRDDyT2DKQHpcPjZ11IWK2hRdQAWJC3u 10 | EnJT9VVw6BqG8LtL1pQC5wJSQo0xC1sJ/MTr/szLvKRjuYZE7YStpUfV6RYq2KQF 11 | 7Oa9nPKtxlIbDCa7z4S6y5yiusYrSSFilK0pVSU+9789kGNZMLzKbnGu+YSVB/Bx 12 | MLXWRAD8DV9zign255pIU/xI5VKjOwID38JfgdcebV/KeCPu8W6jKKbfUsUCqcjL 13 | YjDtHYECgYEA/SaxUoejMOasHppnsAewy/I+DzMuX+KYztqAnzjsuGwRxmxjYyQe 14 | w7EidinM3WuloJIBZzA9aULmWjSKOfTsuGm+Mokucbbw9jaWVT6Co3kWrHySInhZ 15 | sfTwHKz5ojGBcQD4l06xaVM9utNi6r8wvJijFl5xIsMzc5szEkWs9vkCgYEA9285 16 | bGSAAwzUFHVk1pyLKozM2gOtF5rrAUQlWtNVU6K2tw+MKEGara0f+HFZrJZC9Rh2 17 | HBm2U9PPt/kJ73HErQG+E6n0jfol8TQ3ZKz3tlSxImh0CiaKLnh4ahf7o8zU16nT 18 | XDfu3+Rf11EhORXYfZLmdubfCOD4ZaB2/405N3ECgYEA7b4k0gkoLYi1JJiFwD+4 19 | vhBmUAgVCV/ZeoqiOOZRCnITz3GDdVw6uDXm02o2R8wM5Fu6jZo0UmLNyvGEzyFC 20 | H37PbM6Am7LfYZuqW6w1LClQLfVfmJfGROZvib65QqWTlvj+fbsdyniuhIJ5Z1Tf 21 | BH+kyiEvxyHjdDLRJ9vfsKECgYA8P9MFt7sMAxWpHaS+NUQVyk8fTwHY25oZptRJ 22 | t2fxg49mJ90C+GaHn75HKqKhSb1oHNq1oPUqmEreC0AGE/fGAMSd2SZ5Y83VW9eZ 23 | JhzzQtAXBsQqrJO9GQyJGOnnSrsRAIM800nRLrS/ozupwM4EVb7UeQcaDF2vsVEI 24 | jQS/oQKBgHj26xn7AunX5GS8EYe4GPj4VZehmlnEKONGrPrr25aWkaY4kDJgMLUb 25 | AxwIQHbCMm5TMqIxi5l39/O9dxuuGCkOs37j7C3f3VVFuQW1KKyHem9OClgFDZj3 26 | tEEk1N3NevrH06NlmAHweHMuJXL8mBvM375zH9tSw5mgG0OMRbnG 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/ZEO/tests/serverpw.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDiDCCAnCgAwIBAgIJAKjyf/5aIOS8MA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJWQTENMAsGA1UECgwEWk9EQjERMA8GA1UEAwwIem9k 4 | Yi5vcmcxGjAYBgkqhkiG9w0BCQEWC3B3QHpvZGIub3JnMCAXDTE5MDQyNjA5MjIw 5 | NFoYDzMwMDQxMjE4MDkyMjA0WjBYMQswCQYDVQQGEwJVUzELMAkGA1UECAwCVkEx 6 | DTALBgNVBAoMBFpPREIxETAPBgNVBAMMCHpvZGIub3JnMRowGAYJKoZIhvcNAQkB 7 | Fgtwd0B6b2RiLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMrD 8 | +LA3UQ911T1BBD46cIm/0bettM4+3jMRul9NE4gdMwTeSsUo8AusYXZor6gGkfHf 9 | 9Nkc3lS9Yr+OYa0Mv2N6QoIBp4BpUgqURJRLoMjFlj4Vo7et7/V9uYE1HPwKefWt 10 | oDD/hhlEM+9uWigfg23mbu6igYAggM9+3d8ieGYZug6gildzucwZUY5ZeUM3kj2g 11 | Wabi9vLgJQzLcD/TCceBO4w3LtOmODPYT/UxEA7JZDs9liIBKzjJxl9qzzTnw7wM 12 | j6itJjK6b9zN5wjPnY/qNvHkta9woNZpzxBsmgc96W2Fi3Fwwd57b42HoxZbrzwI 13 | HOodJ4UB007d32QyIWkCAwEAAaNTMFEwHQYDVR0OBBYEFDui1OC2+2z2rHADglk5 14 | tGOndxhoMB8GA1UdIwQYMBaAFDui1OC2+2z2rHADglk5tGOndxhoMA8GA1UdEwEB 15 | /wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJ5muBIXZhAXJ4NM79FBV1BLPvQ2 16 | 2RRczEmNGXN35TTzVx1L/h+M8Jbwow40aN/aVjJN0m8+JkJdQZDgnUX+g9ZLPfxU 17 | aAZY8p2nPvMn0AmOmx/DyXyd05WMj+qskyBioPKR79VmhTqSvycnjqORhToKHtrd 18 | n4CNhJr6j0bnwYmLjH5/6x3py37DeS0G5hvBCBkkAP8Ov5NXIOovnCC0FrkOMvhe 19 | CBFt+J4BU/BUJZHsEj1PMMmGadGZm6UGvWptJQOVxdvFlI3QG1vM+bPP9ve7Tshl 20 | rTWzvhfATatBkjnCcuSI0xHRyStauHl4+e9AHH7PI68H19StXnH746aGce0= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /src/ZEO/tests/serverpw_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,769B900D03925712 4 | 5 | z5M/XkqEC1+PxJ1T3QrUhGG9fPTBsPxKy8WlwIytbMg0RXS0oJBiFgNYp1ktqzGo 6 | yT+AdTCRR1hNVX8M5HbV3ksUjKxXKCL3+yaaB6JtGbNRr2qTNwosvxD92nKT/hvN 7 | R6rHF6LcO05s8ubs9b9ON/ja7HCx69N5CjBuCbCFHUTlAXkwD9w0ScrxrtfP50EY 8 | FOw6LAqhhzq6/KO7c1SJ7k9LYzakhL+nbw5KM9QgBk4WHlmKLbCZIZ5RWvu0F4s5 9 | n4qk/BcuXIkbYuEv2kH0nDk5eDfA/dj7xZcMMgL5VFymQzaZLYyj4WuQYXu/7JW/ 10 | nM/ZWBkZOMaI3vnPTG1hJ9pgjLjQnjfNA/bGWwbLxjCsPmR8yvZS4v2iqdB6X3Vl 11 | yJ9aV9r8KoU0PJk3x4v2Zp+RQxgrKSaQw59sXptaXAY3NCRR6ohvq4P5X6UB8r5S 12 | vYdoMeVXhX1hzXeMguln7zQInwJhPZqk4wMIV3lTsCqh1eJ7NC2TGCwble+B/ClR 13 | KtzuJBzoYPLw44ltnXPEMmVH1Fkh17+QZFgZRJrKGD9PGOAXmnzudsZ1xX9kNnOM 14 | JLIT/mzKcqkd8S1n1Xi1x7zGA0Ng5xmKGFe8oDokPJucJO1Ou+hbLDmC0DZUGzr1 15 | qqPJ3F/DzZZDTmD/rZF6doPJgFAZvgpVeiWS8/v1qbz/nz13uwXDLjRPgLfcKpmQ 16 | 4R3V4QlgviDilW61VTZnzV9qAOx4fG6+IwWIGBlrJnfsH/fSCDNlAStc6k12zdun 17 | PIIRJBfbEprGig3vRWUoBASReqow1JCN9DaVCX1P27pDKY5oDe+7/HOrQpwhPoya 18 | 2HEwbKeyY0nCcCXbkWGL1bwEUs/PrJv+61rik4KxOWhKpHWkZLzbozELb44jXrJx 19 | e8K8XKz4La2DEjsUYHc31u6T69GBQO9JDEvih15phUWq8ITvDnkHpAg+wYb1JAHD 20 | QcqDtAulMvT/ZGN0h7qdwbHMggEsLgCCVPG4iZ5K4cXsMbePFvQqq+o4FTMF+cM5 21 | 2Dq0wir92U9cH+ooy80LIt5Kp5zqgQZzr73o9MEgwqJocCrx9ZrofKRUmTV+ZU0r 22 | w5mfUM47Ctnqia0UNGx6SUs3CHFDPWPbzrAaqGzSvFhzR1MMoL1/rJzP1VSm3Fk3 23 | ESWkPrg0J8dcQP/ch9MhH8eoQYyA+2q1vClUbeZLAs5KoHxgi6pSkGYqFhshrA+t 24 | 2AIrUPDPPDf0PgRoXJrzdVOiNNY1rzyql+0JqDH6DjCVcAADWY+48p9U2YFTd7Je 25 | DvnZWihwe0qYGn1AKIkvJ4SR3bQg36etrxhMrMl/8lUn2dnT7GFrhjr9HwCpJwa7 26 | 8tv150SrQXt3FXZCHb+RMUgoWZDeksDohPiGzXkPU6kaSviZVnRMslyU4ahWp6vC 27 | 8tYUhb7K6N+is1hYkICNt6zLl2vBDuCDWmiIwopHtnH1kz8bYlp4/GBVaMIgZiCM 28 | gM/7+p4YCc++s2sJiQ9+BqPo0zKm3bbSP+fPpeWefQVte9Jx4S36YXU52HsJxBTN 29 | WUdHABC+aS2A45I12xMNzOJR6VfxnG6f3JLpt3MkUCEg+898vJGope+TJUhD+aJC 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /src/ZEO/tests/servertesting.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | 15 | # Testing the current ZEO implementation is rather hard due to the 16 | # architecture, which mixes concerns, especially between application 17 | # and networking. Still, it's not as bad as it could be. 18 | 19 | # The 2 most important classes in the architecture are ZEOStorage and 20 | # StorageServer. A ZEOStorage is created for each client connection. 21 | # The StorageServer maintains data shared or needed for coordination 22 | # among clients. 23 | 24 | # The other important part of the architecture is connections. 25 | # Connections are used by ZEOStorages to send messages or return data 26 | # to clients. 27 | 28 | # Here, we'll try to provide some testing infrastructure to isolate 29 | # servers from the network. 30 | 31 | import ZODB.MappingStorage 32 | 33 | import ZEO.asyncio.tests 34 | import ZEO.StorageServer 35 | 36 | 37 | class StorageServer(ZEO.StorageServer.StorageServer): 38 | def __init__(self, addr='test_addr', storages=None, **kw): 39 | if storages is None: 40 | storages = {'1': ZODB.MappingStorage.MappingStorage()} 41 | ZEO.StorageServer.StorageServer.__init__(self, addr, storages, **kw) 42 | 43 | def close(self): 44 | if self.__closed: 45 | return 46 | # instances are typically not run in their own thread 47 | # therefore, the loop usually does not run and the 48 | # normal ``close`` does not work. 49 | loop = self.get_loop() 50 | if loop.is_running(): 51 | return super().close() 52 | loop.call_soon_threadsafe(super().close) 53 | loop.run_forever() # will stop automatically 54 | loop.close() 55 | 56 | def get_loop(self): 57 | return self.acceptor.event_loop # might not work for ``MTAcceptor`` 58 | 59 | 60 | def client(server, name='client'): 61 | zs = ZEO.StorageServer.ZEOStorage(server) 62 | protocol = ZEO.asyncio.tests.server_protocol( 63 | False, zs, protocol_version=b'Z5', addr='test-addr-%s' % name) 64 | 65 | # ``server_protocol`` uses its own testing loop (not 66 | # that of *server*). As a consequence, ``protocol.close`` 67 | # does not work correctly. 68 | # In addition, the artificial loop needs to get closed. 69 | pr_close = protocol.close 70 | 71 | def close(*args, **kw): 72 | pr_close() 73 | zs.notify_disconnected() 74 | protocol.loop.close() 75 | del protocol.close # break reference cycle 76 | 77 | protocol.close = close # install proper closing 78 | zs.notify_connected(protocol) 79 | zs.register('1', 0) 80 | return zs 81 | -------------------------------------------------------------------------------- /src/ZEO/tests/stress.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | """A ZEO client-server stress test to look for leaks. 15 | 16 | The stress test should run in an infinite loop and should involve 17 | multiple connections. 18 | """ 19 | # TODO: This code is currently broken. 20 | 21 | import os 22 | import random 23 | 24 | import transaction 25 | import ZODB 26 | from ZODB.MappingStorage import MappingStorage 27 | from ZODB.tests import MinPO 28 | 29 | from ZEO.ClientStorage import ClientStorage 30 | from ZEO.tests import forker 31 | 32 | 33 | NUM_TRANSACTIONS_PER_CONN = 10 34 | NUM_CONNECTIONS = 10 35 | NUM_ROOTS = 20 36 | MAX_DEPTH = 20 37 | MIN_OBJSIZE = 128 38 | MAX_OBJSIZE = 2048 39 | 40 | 41 | def an_object(): 42 | """Return an object suitable for a PersistentMapping key""" 43 | size = random.randrange(MIN_OBJSIZE, MAX_OBJSIZE) 44 | if os.path.exists("/dev/urandom"): 45 | fp = open("/dev/urandom") 46 | buf = fp.read(size) 47 | fp.close() 48 | return buf 49 | else: 50 | fp = open(MinPO.__file__) 51 | lst = list(fp.read(size)) 52 | fp.close() 53 | random.shuffle(lst) 54 | return "".join(lst) 55 | 56 | 57 | def setup(cn): 58 | """Initialize the database with some objects""" 59 | root = cn.root() 60 | for i in range(NUM_ROOTS): 61 | prev = an_object() 62 | for j in range(random.randrange(1, MAX_DEPTH)): 63 | o = MinPO.MinPO(prev) 64 | prev = o 65 | root[an_object()] = o 66 | transaction.commit() 67 | cn.close() 68 | 69 | 70 | def work(cn): 71 | """Do some work with a transaction""" 72 | cn.sync() 73 | root = cn.root() 74 | obj = random.choice(root.values()) 75 | # walk down to the bottom 76 | while not isinstance(obj.value, str): 77 | obj = obj.value 78 | obj.value = an_object() 79 | transaction.commit() 80 | 81 | 82 | def main(): 83 | # Yuck! Need to cleanup forker so that the API is consistent 84 | # across Unix and Windows, at least if that's possible. 85 | if os.name == "nt": 86 | zaddr, tport, pid = forker.start_zeo_server('MappingStorage', ()) 87 | 88 | def exitserver(): 89 | import socket 90 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 91 | s.connect(tport) 92 | s.close() 93 | else: 94 | zaddr = '', random.randrange(20000, 30000) 95 | pid, exitobj = forker.start_zeo_server(MappingStorage(), zaddr) 96 | 97 | def exitserver(): 98 | exitobj.close() 99 | 100 | while 1: 101 | pid = start_child(zaddr) 102 | print("started", pid) 103 | os.waitpid(pid, 0) 104 | 105 | exitserver() 106 | 107 | 108 | def start_child(zaddr): 109 | 110 | pid = os.fork() 111 | if pid != 0: 112 | return pid 113 | try: 114 | _start_child(zaddr) 115 | finally: 116 | os._exit(0) 117 | 118 | 119 | def _start_child(zaddr): 120 | storage = ClientStorage(zaddr, debug=1, min_disconnect_poll=0.5, wait=1) 121 | db = ZODB.DB(storage, pool_size=NUM_CONNECTIONS) 122 | setup(db.open()) 123 | conns = [] 124 | conn_count = 0 125 | 126 | for i in range(NUM_CONNECTIONS): 127 | c = db.open() 128 | c.__count = 0 129 | conns.append(c) 130 | conn_count += 1 131 | 132 | while conn_count < 25: 133 | c = random.choice(conns) 134 | if c.__count > NUM_TRANSACTIONS_PER_CONN: 135 | conns.remove(c) 136 | c.close() 137 | conn_count += 1 138 | c = db.open() 139 | c.__count = 0 140 | conns.append(c) 141 | else: 142 | c.__count += 1 143 | work(c) 144 | 145 | 146 | if __name__ == "__main__": 147 | main() 148 | -------------------------------------------------------------------------------- /src/ZEO/tests/testConfig.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2003 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | 15 | import unittest 16 | 17 | from ZODB.config import storageFromString 18 | from zope.testing import setupstack 19 | 20 | from .forker import start_zeo_server 21 | from .threaded import threaded_server_tests 22 | 23 | 24 | class ZEOConfigTestBase(setupstack.TestCase): 25 | 26 | setUp = setupstack.setUpDirectory 27 | 28 | def start_server(self, settings='', **kw): 29 | 30 | for name, value in kw.items(): 31 | settings += '\n{} {}\n'.format(name.replace('_', '-'), value) 32 | 33 | zeo_conf = """ 34 | 35 | address 127.0.0.1:0 36 | %s 37 | 38 | """ % settings 39 | return start_zeo_server("\n\n", 40 | zeo_conf, threaded=True) 41 | 42 | def start_client(self, addr, settings='', **kw): 43 | settings += '\nserver %s:%s\n' % addr 44 | for name, value in kw.items(): 45 | settings += '\n{} {}\n'.format(name.replace('_', '-'), value) 46 | return storageFromString( 47 | """ 48 | %import ZEO 49 | 50 | 51 | {} 52 | 53 | """.format(settings)) 54 | 55 | def _client_assertions(self, client, addr, 56 | connected=True, 57 | cache_size=20 * (1 << 20), 58 | cache_path=None, 59 | blob_dir=None, 60 | shared_blob_dir=False, 61 | blob_cache_size=None, 62 | blob_cache_size_check=10, 63 | read_only=False, 64 | read_only_fallback=False, 65 | server_sync=False, 66 | wait_timeout=30, 67 | client_label=None, 68 | storage='1', 69 | name=None): 70 | self.assertEqual(client.is_connected(), connected) 71 | self.assertEqual(client._addr, [addr]) 72 | self.assertEqual(client._cache.maxsize, cache_size) 73 | 74 | self.assertEqual(client._cache.path, cache_path) 75 | self.assertEqual(client.blob_dir, blob_dir) 76 | self.assertEqual(client.shared_blob_dir, shared_blob_dir) 77 | self.assertEqual(client._blob_cache_size, blob_cache_size) 78 | if blob_cache_size: 79 | self.assertEqual(client._blob_cache_size_check, 80 | blob_cache_size * blob_cache_size_check // 100) 81 | self.assertEqual(client._is_read_only, read_only) 82 | self.assertEqual(client._read_only_fallback, read_only_fallback) 83 | self.assertEqual(client._server.timeout, wait_timeout) 84 | self.assertEqual(client._client_label, client_label) 85 | self.assertEqual(client._storage, storage) 86 | self.assertEqual(client.__name__, 87 | name if name is not None else str(client._addr)) 88 | 89 | 90 | class ZEOConfigTest(ZEOConfigTestBase): 91 | 92 | def test_default_zeo_config(self, **client_settings): 93 | addr, stop = self.start_server() 94 | 95 | client = self.start_client(addr, **client_settings) 96 | self._client_assertions(client, addr, **client_settings) 97 | 98 | client.close() 99 | stop() 100 | 101 | def test_client_variations(self): 102 | 103 | for name, value in dict(cache_size=4200, 104 | cache_path='test', 105 | blob_dir='blobs', 106 | blob_cache_size=424242, 107 | read_only=True, 108 | read_only_fallback=True, 109 | server_sync=True, 110 | wait_timeout=33, 111 | client_label='test_client', 112 | name='Test', 113 | ).items(): 114 | params = {name: value} 115 | self.test_default_zeo_config(**params) 116 | 117 | def test_blob_cache_size_check(self): 118 | self.test_default_zeo_config(blob_cache_size=424242, 119 | blob_cache_size_check=50) 120 | 121 | 122 | def test_suite(): 123 | suite = unittest.defaultTestLoader.loadTestsFromTestCase(ZEOConfigTest) 124 | suite.layer = threaded_server_tests 125 | return suite 126 | -------------------------------------------------------------------------------- /src/ZEO/tests/testConversionSupport.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2006 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | import doctest 15 | 16 | 17 | class FakeStorageBase: 18 | 19 | def __getattr__(self, name): 20 | if name in ('getTid', 'history', 'load', 'loadSerial', 21 | 'lastTransaction', 'getSize', 'getName', 'supportsUndo', 22 | 'tpc_transaction'): 23 | return lambda *a, **k: None 24 | raise AttributeError(name) 25 | 26 | def isReadOnly(self): 27 | return False 28 | 29 | def __len__(self): 30 | return 4 31 | 32 | 33 | class FakeStorage(FakeStorageBase): 34 | 35 | def record_iternext(self, next=None): 36 | if next is None: 37 | next = '0' 38 | next = str(int(next) + 1) 39 | oid = next 40 | if next == '4': 41 | next = None 42 | 43 | return oid, oid * 8, 'data ' + oid, next 44 | 45 | 46 | class FakeServer: 47 | storages = { 48 | '1': FakeStorage(), 49 | '2': FakeStorageBase(), 50 | } 51 | lock_managers = storages 52 | 53 | def register_connection(*args): 54 | return None, None 55 | 56 | client_conflict_resolution = False 57 | 58 | 59 | class FakeConnection: 60 | protocol_version = b'Z5' 61 | addr = 'test' 62 | 63 | def call_soon_threadsafe(f, *a): 64 | return f(*a) 65 | 66 | async_ = async_threadsafe = None 67 | 68 | 69 | def test_server_record_iternext(): 70 | """ 71 | 72 | On the server, record_iternext calls are simply delegated to the 73 | underlying storage. 74 | 75 | >>> import ZEO.StorageServer 76 | 77 | >>> zeo = ZEO.StorageServer.ZEOStorage(FakeServer(), False) 78 | >>> zeo.notify_connected(FakeConnection()) 79 | >>> zeo.register('1', False) 80 | 81 | >>> next = None 82 | >>> while 1: 83 | ... oid, serial, data, next = zeo.record_iternext(next) 84 | ... print(oid) 85 | ... if next is None: 86 | ... break 87 | 1 88 | 2 89 | 3 90 | 4 91 | 92 | The storage info also reflects the fact that record_iternext is supported. 93 | 94 | >>> zeo.get_info()['supports_record_iternext'] 95 | True 96 | 97 | >>> zeo = ZEO.StorageServer.ZEOStorage(FakeServer(), False) 98 | >>> zeo.notify_connected(FakeConnection()) 99 | >>> zeo.register('2', False) 100 | 101 | >>> zeo.get_info()['supports_record_iternext'] 102 | False 103 | 104 | """ 105 | 106 | 107 | def test_client_record_iternext(): 108 | """Test client storage delegation to the network client 109 | 110 | The client simply delegates record_iternext calls to it's server stub. 111 | 112 | There's really no decent way to test ZEO without running too much crazy 113 | stuff. I'd rather do a lame test than a really lame test, so here goes. 114 | 115 | First, fake out the connection manager so we can make a connection: 116 | 117 | >>> import ZEO 118 | 119 | >>> class Client(ZEO.asyncio.testing.ClientRunner): 120 | ... 121 | ... def record_iternext(self, next=None): 122 | ... if next == None: 123 | ... next = '0' 124 | ... next = str(int(next) + 1) 125 | ... oid = next 126 | ... if next == '4': 127 | ... next = None 128 | ... 129 | ... return oid, oid*8, 'data ' + oid, next 130 | ... 131 | 132 | >>> client = ZEO.client( 133 | ... '', wait=False, _client_factory=Client) 134 | 135 | Now we'll have our way with it's private _server attr: 136 | 137 | >>> next = None 138 | >>> while 1: 139 | ... oid, serial, data, next = client.record_iternext(next) 140 | ... print(oid) 141 | ... if next is None: 142 | ... break 143 | 1 144 | 2 145 | 3 146 | 4 147 | >>> client.close() 148 | 149 | """ 150 | 151 | 152 | def test_suite(): 153 | return doctest.DocTestSuite() 154 | -------------------------------------------------------------------------------- /src/ZEO/tests/testTransactionBuffer.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | import random 15 | import unittest 16 | 17 | from ZEO.TransactionBuffer import TransactionBuffer 18 | 19 | 20 | def random_string(size): 21 | """Return a random string of size size.""" 22 | lst = [chr(random.randrange(256)) for i in range(size)] 23 | return "".join(lst) 24 | 25 | 26 | def new_store_data(): 27 | """Return arbitrary data to use as argument to store() method.""" 28 | return random_string(8), random_string(random.randrange(1000)) 29 | 30 | 31 | def store(tbuf, resolved=False): 32 | data = new_store_data() 33 | tbuf.store(*data) 34 | if resolved: 35 | tbuf.server_resolve(data[0]) 36 | return data 37 | 38 | 39 | class TransBufTests(unittest.TestCase): 40 | 41 | def checkTypicalUsage(self): 42 | tbuf = TransactionBuffer(0) 43 | store(tbuf) 44 | store(tbuf) 45 | for o in tbuf: 46 | pass 47 | tbuf.close() 48 | 49 | def checkOrderPreserved(self): 50 | tbuf = TransactionBuffer(0) 51 | data = [] 52 | for i in range(10): 53 | data.append((store(tbuf), False)) 54 | data.append((store(tbuf, True), True)) 55 | 56 | for i, (oid, d, resolved) in enumerate(tbuf): 57 | self.assertEqual((oid, d), data[i][0]) 58 | self.assertEqual(resolved, data[i][1]) 59 | tbuf.close() 60 | 61 | 62 | def test_suite(): 63 | test_loader = unittest.TestLoader() 64 | test_loader.testMethodPrefix = 'check' 65 | return test_loader.loadTestsFromTestCase(TransBufTests) 66 | -------------------------------------------------------------------------------- /src/ZEO/tests/testZEOOptions.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2001, 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE 12 | # 13 | ############################################################################## 14 | 15 | """Test suite for ZEO.runzeo.ZEOOptions.""" 16 | 17 | import os 18 | import tempfile 19 | 20 | import ZODB.config 21 | from zdaemon.tests.testzdoptions import TestZDOptions 22 | 23 | from ZEO.runzeo import ZEOOptions 24 | 25 | 26 | # When a hostname isn't specified in a socket binding address, ZConfig 27 | # supplies the empty string. 28 | DEFAULT_BINDING_HOST = "" 29 | 30 | 31 | class TestZEOOptions(TestZDOptions): 32 | 33 | OptionsClass = ZEOOptions 34 | 35 | input_args = ["-f", "Data.fs", "-a", "5555"] 36 | output_opts = [("-f", "Data.fs"), ("-a", "5555")] 37 | output_args = [] 38 | 39 | configdata = """ 40 | 41 | address 5555 42 | 43 | 44 | path Data.fs 45 | 46 | """ 47 | 48 | def setUp(self): 49 | self.tempfilename = tempfile.mktemp() 50 | with open(self.tempfilename, "w") as f: 51 | f.write(self.configdata) 52 | 53 | def tearDown(self): 54 | try: 55 | os.remove(self.tempfilename) 56 | except OSError: 57 | pass 58 | 59 | def test_configure(self): 60 | # Hide the base class test_configure 61 | pass 62 | 63 | def test_default_help(self): pass # disable silly test w spurious failures 64 | 65 | def test_defaults_with_schema(self): 66 | options = self.OptionsClass() 67 | options.realize(["-C", self.tempfilename]) 68 | self.assertEqual(options.address, (DEFAULT_BINDING_HOST, 5555)) 69 | self.assertEqual(len(options.storages), 1) 70 | opener = options.storages[0] 71 | self.assertEqual(opener.name, "fs") 72 | self.assertEqual(opener.__class__, ZODB.config.FileStorage) 73 | self.assertEqual(options.read_only, 0) 74 | self.assertEqual(options.transaction_timeout, None) 75 | self.assertEqual(options.invalidation_queue_size, 100) 76 | 77 | def test_defaults_without_schema(self): 78 | options = self.OptionsClass() 79 | options.realize(["-a", "5555", "-f", "Data.fs"]) 80 | self.assertEqual(options.address, (DEFAULT_BINDING_HOST, 5555)) 81 | self.assertEqual(len(options.storages), 1) 82 | opener = options.storages[0] 83 | self.assertEqual(opener.name, "1") 84 | self.assertEqual(opener.__class__, ZODB.config.FileStorage) 85 | self.assertEqual(opener.config.path, "Data.fs") 86 | self.assertEqual(options.read_only, 0) 87 | self.assertEqual(options.transaction_timeout, None) 88 | self.assertEqual(options.invalidation_queue_size, 100) 89 | 90 | def test_commandline_overrides(self): 91 | options = self.OptionsClass() 92 | options.realize(["-C", self.tempfilename, 93 | "-a", "6666", "-f", "Wisdom.fs"]) 94 | self.assertEqual(options.address, (DEFAULT_BINDING_HOST, 6666)) 95 | self.assertEqual(len(options.storages), 1) 96 | opener = options.storages[0] 97 | self.assertEqual(opener.__class__, ZODB.config.FileStorage) 98 | self.assertEqual(opener.config.path, "Wisdom.fs") 99 | self.assertEqual(options.read_only, 0) 100 | self.assertEqual(options.transaction_timeout, None) 101 | self.assertEqual(options.invalidation_queue_size, 100) 102 | 103 | 104 | del TestZDOptions # don't run ZDaemon tests 105 | -------------------------------------------------------------------------------- /src/ZEO/tests/testZEOServer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from ZEO.runzeo import ZEOServer 5 | 6 | 7 | class TestStorageServer: 8 | 9 | def __init__(self, fail_create_server): 10 | self.called = [] 11 | if fail_create_server: 12 | raise RuntimeError() 13 | 14 | def close(self): 15 | self.called.append("close") 16 | 17 | 18 | class TestZEOServer(ZEOServer): 19 | 20 | def __init__(self, fail_create_server=False, fail_loop_forever=False): 21 | ZEOServer.__init__(self, None) 22 | self.called = [] 23 | self.fail_create_server = fail_create_server 24 | self.fail_loop_forever = fail_loop_forever 25 | 26 | def setup_default_logging(self): 27 | self.called.append("setup_default_logging") 28 | 29 | def check_socket(self): 30 | self.called.append("check_socket") 31 | 32 | def clear_socket(self): 33 | self.called.append("clear_socket") 34 | 35 | def make_pidfile(self): 36 | self.called.append("make_pidfile") 37 | 38 | def open_storages(self): 39 | self.called.append("open_storages") 40 | 41 | def setup_signals(self): 42 | self.called.append("setup_signals") 43 | 44 | def create_server(self): 45 | self.called.append("create_server") 46 | self.server = TestStorageServer(self.fail_create_server) 47 | 48 | def loop_forever(self): 49 | self.called.append("loop_forever") 50 | if self.fail_loop_forever: 51 | raise RuntimeError() 52 | 53 | def close_server(self): 54 | self.called.append("close_server") 55 | ZEOServer.close_server(self) 56 | 57 | def remove_pidfile(self): 58 | self.called.append("remove_pidfile") 59 | 60 | 61 | class AttributeErrorTests(unittest.TestCase): 62 | 63 | def testFailCreateServer(self): 64 | # 65 | # Fix AttributeError: 'ZEOServer' object has no attribute 66 | # 'server' in ZEOServer.main 67 | # 68 | # Demonstrate the AttributeError 69 | zeo = TestZEOServer(fail_create_server=True) 70 | self.assertRaises(RuntimeError, zeo.main) 71 | 72 | 73 | class CloseServerTests(unittest.TestCase): 74 | 75 | def testCallSequence(self): 76 | # The close_server hook is called after loop_forever 77 | # has returned 78 | zeo = TestZEOServer() 79 | zeo.main() 80 | self.assertEqual(zeo.called, [ 81 | "setup_default_logging", 82 | "check_socket", 83 | "clear_socket", 84 | "make_pidfile", 85 | "open_storages", 86 | "setup_signals", 87 | "create_server", 88 | "loop_forever", 89 | "close_server", # New 90 | "clear_socket", 91 | "remove_pidfile", 92 | ]) 93 | # The default implementation closes the storage server 94 | self.assertEqual(hasattr(zeo, "server"), True) 95 | self.assertEqual(zeo.server.called, ["close"]) 96 | 97 | def testFailLoopForever(self): 98 | # The close_server hook is called if loop_forever exits 99 | # with an exception 100 | zeo = TestZEOServer(fail_loop_forever=True) 101 | self.assertRaises(RuntimeError, zeo.main) 102 | self.assertEqual(zeo.called, [ 103 | "setup_default_logging", 104 | "check_socket", 105 | "clear_socket", 106 | "make_pidfile", 107 | "open_storages", 108 | "setup_signals", 109 | "create_server", 110 | "loop_forever", 111 | "close_server", 112 | "clear_socket", 113 | "remove_pidfile", 114 | ]) 115 | # The storage server has been closed 116 | self.assertEqual(hasattr(zeo, "server"), True) 117 | self.assertEqual(zeo.server.called, ["close"]) 118 | 119 | def testFailCreateServer(self): 120 | # The close_server hook is called if create_server exits 121 | # with an exception 122 | zeo = TestZEOServer(fail_create_server=True) 123 | self.assertRaises(RuntimeError, zeo.main) 124 | self.assertEqual(zeo.called, [ 125 | "setup_default_logging", 126 | "check_socket", 127 | "clear_socket", 128 | "make_pidfile", 129 | "open_storages", 130 | "setup_signals", 131 | "create_server", 132 | "close_server", 133 | "clear_socket", 134 | "remove_pidfile", 135 | ]) 136 | # The server attribute is present but None 137 | self.assertEqual(hasattr(zeo, "server"), True) 138 | self.assertEqual(zeo.server, None) 139 | 140 | 141 | @mock.patch('os.unlink') 142 | class TestZEOServerSocket(unittest.TestCase): 143 | 144 | def _unlinked(self, unlink, options): 145 | server = ZEOServer(options) 146 | server.clear_socket() 147 | unlink.assert_called_once() 148 | 149 | def _not_unlinked(self, unlink, options): 150 | server = ZEOServer(options) 151 | server.clear_socket() 152 | unlink.assert_not_called() 153 | 154 | def test_clear_with_native_str(self, unlink): 155 | class Options: 156 | address = "a str that does not exist" 157 | self._unlinked(unlink, Options) 158 | 159 | def test_clear_with_unicode_str(self, unlink): 160 | class Options: 161 | address = "a str that does not exist" 162 | self._unlinked(unlink, Options) 163 | 164 | def test_clear_with_bytes(self, unlink): 165 | class Options: 166 | address = b'a byte str that does not exist' 167 | 168 | self._not_unlinked(unlink, Options) 169 | 170 | def test_clear_with_tuple(self, unlink): 171 | class Options: 172 | address = ('abc', 1) 173 | self._not_unlinked(unlink, Options) 174 | -------------------------------------------------------------------------------- /src/ZEO/tests/test_client_credentials.py: -------------------------------------------------------------------------------- 1 | """Clients can pass credentials to a server. 2 | 3 | This is an experimental feature to enable server authentication and 4 | authorization. 5 | """ 6 | import unittest 7 | 8 | from zope.testing import setupstack 9 | 10 | import ZEO.StorageServer 11 | 12 | from .threaded import threaded_server_tests 13 | 14 | 15 | class ClientAuthTests(setupstack.TestCase): 16 | 17 | def setUp(self): 18 | self.setUpDirectory() 19 | self.__register = ZEO.StorageServer.ZEOStorage.register 20 | 21 | def tearDown(self): 22 | ZEO.StorageServer.ZEOStorage.register = self.__register 23 | 24 | def test_passing_credentials(self): 25 | 26 | # First, we'll temporarily swap the storage server register 27 | # method with one that let's is see credentials that were passed: 28 | 29 | creds_log = [] 30 | 31 | def register(zs, storage_id, read_only, credentials=self): 32 | creds_log.append(credentials) 33 | return self.__register(zs, storage_id, read_only) 34 | 35 | ZEO.StorageServer.ZEOStorage.register = register 36 | 37 | # Now start an in process server 38 | addr, stop = ZEO.server() 39 | 40 | # If we connect, without providing credentials, then no 41 | # credentials will be passed to register: 42 | 43 | client = ZEO.client(addr) 44 | 45 | self.assertEqual(creds_log, [self]) 46 | client.close() 47 | creds_log.pop() 48 | 49 | # Even if we pass credentials, they'll be ignored 50 | creds = dict(user='me', password='123') 51 | client = ZEO.client(addr, credentials=creds) 52 | self.assertEqual(creds_log, [self]) 53 | client.close() 54 | 55 | stop() 56 | 57 | 58 | def test_suite(): 59 | suite = unittest.defaultTestLoader.loadTestsFromTestCase(ClientAuthTests) 60 | suite.layer = threaded_server_tests 61 | return suite 62 | -------------------------------------------------------------------------------- /src/ZEO/tests/test_client_side_conflict_resolution.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | 5 | import zope.testing.setupstack 6 | from BTrees.Length import Length 7 | from ZODB import serialize 8 | from ZODB.broken import find_global 9 | from ZODB.DemoStorage import DemoStorage 10 | from ZODB.utils import maxtid 11 | from ZODB.utils import z64 12 | 13 | import ZEO 14 | 15 | from .utils import StorageServer 16 | 17 | 18 | class Var: 19 | def __eq__(self, other): 20 | self.value = other 21 | return True 22 | 23 | 24 | class ClientSideConflictResolutionTests(zope.testing.setupstack.TestCase): 25 | 26 | def test_server_side(self): 27 | # First, verify default conflict resolution. 28 | server = StorageServer(self, DemoStorage()) 29 | zs = server.zs 30 | 31 | reader = serialize.ObjectReader( 32 | factory=lambda conn, *args: find_global(*args)) 33 | writer = serialize.ObjectWriter() 34 | ob = Length(0) 35 | ob._p_oid = z64 36 | 37 | # 2 non-conflicting transactions: 38 | 39 | zs.tpc_begin(1, '', '', {}) 40 | zs.storea(ob._p_oid, z64, writer.serialize(ob), 1) 41 | self.assertEqual(zs.vote(1), []) 42 | tid1 = server.unpack_result(zs.tpc_finish(1)) 43 | server.assert_calls(self, ('info', {'length': 1, 'size': Var()})) 44 | 45 | ob.change(1) 46 | zs.tpc_begin(2, '', '', {}) 47 | zs.storea(ob._p_oid, tid1, writer.serialize(ob), 2) 48 | self.assertEqual(zs.vote(2), []) 49 | tid2 = server.unpack_result(zs.tpc_finish(2)) 50 | server.assert_calls(self, ('info', {'size': Var(), 'length': 1})) 51 | 52 | # Now, a cnflicting one: 53 | zs.tpc_begin(3, '', '', {}) 54 | zs.storea(ob._p_oid, tid1, writer.serialize(ob), 3) 55 | 56 | # Vote returns the object id, indicating that a conflict was resolved. 57 | self.assertEqual(zs.vote(3), [ob._p_oid]) 58 | tid3 = server.unpack_result(zs.tpc_finish(3)) 59 | 60 | p, serial, next_serial = zs.loadBefore(ob._p_oid, maxtid) 61 | self.assertEqual((serial, next_serial), (tid3, None)) 62 | self.assertEqual(reader.getClassName(p), 'BTrees.Length.Length') 63 | self.assertEqual(reader.getState(p), 2) 64 | 65 | # Now, we'll create a server that expects the client to 66 | # resolve conflicts: 67 | 68 | server = StorageServer( 69 | self, DemoStorage(), client_conflict_resolution=True) 70 | zs = server.zs 71 | 72 | # 2 non-conflicting transactions: 73 | 74 | zs.tpc_begin(1, '', '', {}) 75 | zs.storea(ob._p_oid, z64, writer.serialize(ob), 1) 76 | self.assertEqual(zs.vote(1), []) 77 | tid1 = server.unpack_result(zs.tpc_finish(1)) 78 | server.assert_calls(self, ('info', {'size': Var(), 'length': 1})) 79 | 80 | ob.change(1) 81 | zs.tpc_begin(2, '', '', {}) 82 | zs.storea(ob._p_oid, tid1, writer.serialize(ob), 2) 83 | self.assertEqual(zs.vote(2), []) 84 | tid2 = server.unpack_result(zs.tpc_finish(2)) 85 | server.assert_calls(self, ('info', {'length': 1, 'size': Var()})) 86 | 87 | # Now, a conflicting one: 88 | zs.tpc_begin(3, '', '', {}) 89 | zs.storea(ob._p_oid, tid1, writer.serialize(ob), 3) 90 | 91 | # Vote returns an object, indicating that a conflict was not resolved. 92 | self.assertEqual( 93 | zs.vote(3), 94 | [dict(oid=ob._p_oid, 95 | serials=(tid2, tid1), 96 | data=writer.serialize(ob), 97 | )], 98 | ) 99 | 100 | # Now, it's up to the client to resolve the conflict. It can 101 | # do this by making another store call. In this call, we use 102 | # tid2 as the starting tid: 103 | ob.change(1) 104 | zs.storea(ob._p_oid, tid2, writer.serialize(ob), 3) 105 | self.assertEqual(zs.vote(3), []) 106 | tid3 = server.unpack_result(zs.tpc_finish(3)) 107 | server.assert_calls(self, ('info', {'size': Var(), 'length': 1})) 108 | 109 | p, serial, next_serial = zs.loadBefore(ob._p_oid, maxtid) 110 | self.assertEqual((serial, next_serial), (tid3, None)) 111 | self.assertEqual(reader.getClassName(p), 'BTrees.Length.Length') 112 | self.assertEqual(reader.getState(p), 3) 113 | 114 | def test_client_side(self): 115 | # First, traditional: 116 | path = tempfile.mkdtemp(prefix='zeo-test-') 117 | self.addCleanup(shutil.rmtree, path) 118 | addr, stop = ZEO.server(os.path.join(path, 'data.fs'), threaded=False) 119 | db = ZEO.DB(addr, wait_timeout=2) 120 | with db.transaction() as conn: 121 | conn.root.len = Length(0) 122 | conn2 = db.open() 123 | conn2.root.len.change(1) 124 | with db.transaction() as conn: 125 | conn.root.len.change(1) 126 | 127 | conn2.transaction_manager.commit() 128 | 129 | self.assertEqual(conn2.root.len.value, 2) 130 | 131 | db.close() 132 | stop() 133 | 134 | # Now, do conflict resolution on the client. 135 | addr2, stop = ZEO.server( 136 | storage_conf='\n\n', 137 | zeo_conf=dict(client_conflict_resolution=True), 138 | threaded=False, 139 | ) 140 | 141 | db = ZEO.DB(addr2) 142 | with db.transaction() as conn: 143 | conn.root.len = Length(0) 144 | conn2 = db.open() 145 | conn2.root.len.change(1) 146 | with db.transaction() as conn: 147 | conn.root.len.change(1) 148 | 149 | self.assertEqual(conn2.root.len.value, 1) 150 | conn2.transaction_manager.commit() 151 | 152 | self.assertEqual(conn2.root.len.value, 2) 153 | 154 | db.close() 155 | stop() 156 | -------------------------------------------------------------------------------- /src/ZEO/tests/test_marshal.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ZEO.asyncio.marshal import encode 4 | from ZEO.asyncio.marshal import pickle_server_decode 5 | 6 | 7 | try: 8 | from ZopeUndo.Prefix import Prefix 9 | except ModuleNotFoundError: 10 | _HAVE_ZOPE_UNDO = False 11 | else: 12 | _HAVE_ZOPE_UNDO = True 13 | 14 | 15 | class MarshalTests(unittest.TestCase): 16 | 17 | @unittest.skipUnless(_HAVE_ZOPE_UNDO, 'ZopeUndo is not installed') 18 | def testServerDecodeZopeUndoFilter(self): 19 | # this is an example (1) of Zope2's arguments for 20 | # undoInfo call. Arguments are encoded by ZEO client 21 | # and decoded by server. The operation must be idempotent. 22 | # (1) https://github.com/zopefoundation/Zope/blob/2.13/src/App/Undo.py#L111 # NOQA: E501 line too long 23 | args = (0, 20, {'user_name': Prefix('test')}) 24 | # test against repr because Prefix __eq__ operator 25 | # doesn't compare Prefix with Prefix but only 26 | # Prefix with strings. see Prefix.__doc__ 27 | self.assertEqual( 28 | repr(pickle_server_decode(encode(*args))), 29 | repr(args) 30 | ) 31 | -------------------------------------------------------------------------------- /src/ZEO/tests/test_sync.py: -------------------------------------------------------------------------------- 1 | from zope.testing import setupstack 2 | 3 | from ZEO import _forker as forker 4 | 5 | from .. import client 6 | from .. import server 7 | 8 | 9 | class SyncTests(setupstack.TestCase): 10 | 11 | def instrument(self): 12 | self.__ping_calls = 0 13 | 14 | server = getattr(forker, self.__name + '_server') 15 | 16 | [zs] = getattr(server.server, 'zeo_storages_by_storage_id')['1'] 17 | orig_ping = getattr(zs, 'ping') 18 | 19 | def ping(): 20 | self.__ping_calls += 1 21 | return orig_ping() 22 | 23 | setattr(zs, 'ping', ping) 24 | 25 | def test_server_sync(self): 26 | self.__name = 's%s' % id(self) 27 | addr, stop = server(name=self.__name) 28 | 29 | # By default the client sync method is a noop: 30 | c = client(addr, wait_timeout=2) 31 | self.instrument() 32 | c.sync() 33 | self.assertEqual(self.__ping_calls, 0) 34 | c.close() 35 | 36 | # But if we pass server_sync: 37 | c = client(addr, wait_timeout=2, server_sync=True) 38 | self.instrument() 39 | c.sync() 40 | self.assertEqual(self.__ping_calls, 1) 41 | c.close() 42 | 43 | stop() 44 | -------------------------------------------------------------------------------- /src/ZEO/tests/threaded.py: -------------------------------------------------------------------------------- 1 | """Test layer for threaded-server tests 2 | 3 | uvloop currently has a bug, 4 | https://github.com/MagicStack/uvloop/issues/39, that causes failure if 5 | multiprocessing and threaded servers are mixed in the same 6 | application, so we isolate the few threaded tests in their own layer. 7 | """ 8 | import ZODB.tests.util 9 | 10 | 11 | threaded_server_tests = ZODB.tests.util.MininalTestLayer( 12 | 'threaded_server_tests') 13 | -------------------------------------------------------------------------------- /src/ZEO/tests/utils.py: -------------------------------------------------------------------------------- 1 | """Testing helpers 2 | """ 3 | import ZEO.StorageServer 4 | 5 | from ..asyncio.server import best_protocol_version 6 | 7 | 8 | class ServerProtocol: 9 | 10 | method = ('register', ) 11 | 12 | def __init__(self, zs, 13 | protocol_version=best_protocol_version, 14 | addr='test-address'): 15 | self.calls = [] 16 | self.addr = addr 17 | self.zs = zs 18 | self.protocol_version = protocol_version 19 | zs.notify_connected(self) 20 | 21 | closed = False 22 | 23 | def close(self): 24 | if not self.closed: 25 | self.closed = True 26 | self.zs.notify_disconnected() 27 | 28 | def call_soon_threadsafe(self, func, *args): 29 | func(*args) 30 | 31 | def async_(self, *args): 32 | self.calls.append(args) 33 | 34 | async_threadsafe = async_ 35 | 36 | 37 | class StorageServer: 38 | """Create a client interface to a StorageServer. 39 | 40 | This is for testing StorageServer. It interacts with the storgr 41 | server through its network interface, but without creating a 42 | network connection. 43 | """ 44 | 45 | def __init__(self, test, storage, 46 | protocol_version=b'Z' + best_protocol_version, 47 | **kw): 48 | self.test = test 49 | self.storage_server = ZEO.StorageServer.StorageServer( 50 | None, {'1': storage}, **kw) 51 | self.zs = self.storage_server.create_client_handler() 52 | self.protocol = ServerProtocol(self.zs, 53 | protocol_version=protocol_version) 54 | self.zs.register('1', kw.get('read_only', False)) 55 | 56 | def assert_calls(self, test, *argss): 57 | if argss: 58 | for args in argss: 59 | test.assertEqual(self.protocol.calls.pop(0), args) 60 | else: 61 | test.assertEqual(self.protocol.calls, ()) 62 | 63 | def unpack_result(self, result): 64 | """For methods that return Result objects, unwrap the results 65 | """ 66 | result, callback = result.args 67 | callback() 68 | return result 69 | -------------------------------------------------------------------------------- /src/ZEO/tests/zdoptions.test: -------------------------------------------------------------------------------- 1 | Minimal test of Server Options Handling 2 | ======================================= 3 | 4 | This is initially motivated by a desire to remove the requirement of 5 | specifying a storage name when there is only one storage. 6 | 7 | Storage Names 8 | ------------- 9 | 10 | It is an error not to specify any storages: 11 | 12 | >>> import sys, ZEO.runzeo 13 | >>> from io import StringIO 14 | >>> stderr = sys.stderr 15 | 16 | >>> with open('config', 'w') as f: 17 | ... _ = f.write(""" 18 | ... 19 | ... address 8100 20 | ... 21 | ... """) 22 | 23 | >>> sys.stderr = StringIO() 24 | >>> options = ZEO.runzeo.ZEOOptions() 25 | >>> options.realize('-C config'.split()) 26 | Traceback (most recent call last): 27 | ... 28 | SystemExit: 2 29 | 30 | >>> print(sys.stderr.getvalue()) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE 31 | Error: not enough values for section type 'zodb.storage'; 32 | 0 found, 1 required 33 | ... 34 | 35 | 36 | But we can specify a storage without a name: 37 | 38 | >>> with open('config', 'w') as f: 39 | ... _ = f.write(""" 40 | ... 41 | ... address 8100 42 | ... 43 | ... 44 | ... 45 | ... """) 46 | >>> options = ZEO.runzeo.ZEOOptions() 47 | >>> options.realize('-C config'.split()) 48 | >>> [storage.name for storage in options.storages] 49 | ['1'] 50 | 51 | We can't have multiple unnamed storages: 52 | 53 | >>> sys.stderr = StringIO() 54 | >>> with open('config', 'w') as f: 55 | ... _ = f.write(""" 56 | ... 57 | ... address 8100 58 | ... 59 | ... 60 | ... 61 | ... 62 | ... 63 | ... """) 64 | >>> options = ZEO.runzeo.ZEOOptions() 65 | >>> options.realize('-C config'.split()) 66 | Traceback (most recent call last): 67 | ... 68 | SystemExit: 2 69 | 70 | >>> print(sys.stderr.getvalue()) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE 71 | Error: No more than one storage may be unnamed. 72 | ... 73 | 74 | Or an unnamed storage and one named '1': 75 | 76 | >>> sys.stderr = StringIO() 77 | >>> with open('config', 'w') as f: 78 | ... _ = f.write(""" 79 | ... 80 | ... address 8100 81 | ... 82 | ... 83 | ... 84 | ... 85 | ... 86 | ... """) 87 | >>> options = ZEO.runzeo.ZEOOptions() 88 | >>> options.realize('-C config'.split()) 89 | Traceback (most recent call last): 90 | ... 91 | SystemExit: 2 92 | 93 | >>> print(sys.stderr.getvalue()) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE 94 | Error: Can't have an unnamed storage and a storage named 1. 95 | ... 96 | 97 | But we can have multiple storages: 98 | 99 | >>> with open('config', 'w') as f: 100 | ... _ = f.write(""" 101 | ... 102 | ... address 8100 103 | ... 104 | ... 105 | ... 106 | ... 107 | ... 108 | ... """) 109 | >>> options = ZEO.runzeo.ZEOOptions() 110 | >>> options.realize('-C config'.split()) 111 | >>> [storage.name for storage in options.storages] 112 | ['x', 'y'] 113 | 114 | As long as the names are unique: 115 | 116 | >>> sys.stderr = StringIO() 117 | >>> with open('config', 'w') as f: 118 | ... _ = f.write(""" 119 | ... 120 | ... address 8100 121 | ... 122 | ... 123 | ... 124 | ... 125 | ... 126 | ... """) 127 | >>> options = ZEO.runzeo.ZEOOptions() 128 | >>> options.realize('-C config'.split()) 129 | Traceback (most recent call last): 130 | ... 131 | SystemExit: 2 132 | 133 | >>> print(sys.stderr.getvalue()) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE 134 | Error: section names must not be re-used within the same container:'1' 135 | ... 136 | 137 | .. Cleanup ===================================================== 138 | 139 | >>> sys.stderr = stderr 140 | -------------------------------------------------------------------------------- /src/ZEO/tests/zeo-fan-out.test: -------------------------------------------------------------------------------- 1 | ZEO Fan Out 2 | =========== 3 | 4 | We should be able to set up ZEO servers with ZEO clients. Let's see 5 | if we can make it work. 6 | 7 | We'll use some helper functions. The first is a helper that starts 8 | ZEO servers for us and another one that picks ports. 9 | 10 | We'll start the first server: 11 | 12 | >>> (_, port0), adminaddr0 = start_server( 13 | ... '\npath fs\nblob-dir blobs\n', keep=1) 14 | 15 | Then we'll start 2 others that use this one: 16 | 17 | >>> addr1, _ = start_server( 18 | ... '\nserver %s\nblob-dir b1\n' % port0) 19 | >>> addr2, _ = start_server( 20 | ... '\nserver %s\nblob-dir b2\n' % port0) 21 | 22 | 23 | Now, let's create some client storages that connect to these: 24 | 25 | >>> import os, ZEO, ZODB.blob, ZODB.POSException, transaction 26 | 27 | >>> db0 = ZEO.DB(port0, blob_dir='cb0') 28 | >>> db1 = ZEO.DB(addr1, blob_dir='cb1') 29 | >>> tm1 = transaction.TransactionManager() 30 | >>> c1 = db1.open(transaction_manager=tm1) 31 | >>> r1 = c1.root() 32 | >>> r1 33 | {} 34 | 35 | >>> db2 = ZEO.DB(addr2, blob_dir='cb2') 36 | >>> tm2 = transaction.TransactionManager() 37 | >>> c2 = db2.open(transaction_manager=tm2) 38 | >>> r2 = c2.root() 39 | >>> r2 40 | {} 41 | 42 | If we update c1, we'll eventually see the change in c2: 43 | 44 | >>> import persistent.mapping 45 | 46 | >>> r1[1] = persistent.mapping.PersistentMapping() 47 | >>> r1[1].v = 1000 48 | >>> r1[2] = persistent.mapping.PersistentMapping() 49 | >>> r1[2].v = -1000 50 | >>> r1[3] = ZODB.blob.Blob(b'x'*4111222) 51 | >>> for i in range(1000, 2000): 52 | ... r1[i] = persistent.mapping.PersistentMapping() 53 | ... r1[i].v = 0 54 | >>> tm1.commit() 55 | >>> blob_id = r1[3]._p_oid, r1[1]._p_serial 56 | 57 | >>> import time 58 | >>> for i in range(100): 59 | ... t = tm2.begin() 60 | ... if 1 in r2: 61 | ... break 62 | ... time.sleep(0.01) 63 | >>> tm2.abort() 64 | 65 | 66 | >>> r2[1].v 67 | 1000 68 | 69 | >>> r2[2].v 70 | -1000 71 | 72 | Now, let's see if we can break it. :) 73 | 74 | >>> def f(): 75 | ... c = db1.open(transaction.TransactionManager()) 76 | ... r = c.root() 77 | ... i = 0 78 | ... while i < 100: 79 | ... r[1].v -= 1 80 | ... r[2].v += 1 81 | ... try: 82 | ... c.transaction_manager.commit() 83 | ... i += 1 84 | ... except ZODB.POSException.ConflictError: 85 | ... c.transaction_manager.abort() 86 | ... c.close() 87 | 88 | >>> import threading 89 | >>> threadf = threading.Thread(target=f) 90 | >>> threadg = threading.Thread(target=f) 91 | >>> threadf.start() 92 | 93 | >>> threadg.start() 94 | 95 | >>> s2 = db2.storage 96 | >>> start_time = time.time() 97 | >>> import os 98 | >>> from ZEO.ClientStorage import _lock_blob 99 | >>> while time.time() - start_time < 999: 100 | ... t = tm2.begin() 101 | ... if r2[1].v + r2[2].v: 102 | ... print('oops', r2[1], r2[2]) 103 | ... if r2[1].v == 800: 104 | ... break # we caught up 105 | ... path = s2.fshelper.getBlobFilename(*blob_id) 106 | ... if os.path.exists(path): 107 | ... ZODB.blob.remove_committed(path) 108 | ... _ = s2.fshelper.createPathForOID(blob_id[0]) 109 | ... blob_lock = _lock_blob(path) 110 | ... try: 111 | ... future = s2._call('sendBlob', *blob_id, wait=False) 112 | ... future.result(2) # wait at most 2s for completion 113 | ... finally: 114 | ... blob_lock.close() 115 | ... else: print('Dang') 116 | 117 | >>> threadf.join() 118 | 119 | >>> threadg.join() 120 | 121 | If we shutdown and restart the source server, the variables will be 122 | invalidated: 123 | 124 | >>> stop_server(adminaddr0) 125 | >>> _ = start_server('\npath fs\n\n', 126 | ... port=port0) 127 | >>> time.sleep(1) # get past startup / verification 128 | 129 | >>> for i in range(1000): 130 | ... c1.sync() 131 | ... c2.sync() 132 | ... if ( 133 | ... (r1[1]._p_changed is None) 134 | ... and 135 | ... (r1[2]._p_changed is None) 136 | ... and 137 | ... (r2[1]._p_changed is None) 138 | ... and 139 | ... (r2[2]._p_changed is None) 140 | ... ): 141 | ... print('Cool') 142 | ... break 143 | ... time.sleep(0.01) 144 | ... else: 145 | ... print('Dang') 146 | Cool 147 | 148 | Cleanup: 149 | 150 | >>> db0.close() 151 | >>> db1.close() 152 | >>> db2.close() 153 | -------------------------------------------------------------------------------- /src/ZEO/tests/zeo_blob_cache.test: -------------------------------------------------------------------------------- 1 | ZEO caching of blob data 2 | ======================== 3 | 4 | ZEO supports 2 modes for providing clients access to blob data: 5 | 6 | shared 7 | Blob data are shared via a network file system. The client shares 8 | a common blob directory with the server. 9 | 10 | non-shared 11 | Blob data are loaded from the storage server and cached locally. 12 | A maximum size for the blob data can be set and data are removed 13 | when the size is exceeded. 14 | 15 | In this test, we'll demonstrate that blobs data are removed from a ZEO 16 | cache when the amount of data stored exceeds a given limit. 17 | 18 | Let's start by setting up some data: 19 | 20 | >>> addr, _ = start_server(blob_dir='server-blobs') 21 | 22 | We'll also create a client. 23 | 24 | >>> import ZEO 25 | >>> db = ZEO.DB(addr, blob_dir='blobs', blob_cache_size=3000) 26 | 27 | Here, we passed a blob_cache_size parameter, which specifies a target 28 | blob cache size. This is not a hard limit, but rather a target. It 29 | defaults to a very large value. We also passed a blob_cache_size_check 30 | option. The blob_cache_size_check option specifies the number of 31 | bytes, as a percent of the target that can be written or downloaded 32 | from the server before the cache size is checked. The 33 | blob_cache_size_check option defaults to 100. We passed 10, to check 34 | after writing 10% of the target size. 35 | 36 | .. We're going to wait for any threads we started to finish, so... 37 | 38 | >>> import threading 39 | >>> old_threads = list(threading.enumerate()) 40 | 41 | We want to check for name collections in the blob cache dir. We'll try 42 | to provoke name collections by reducing the number of cache directory 43 | subdirectories. 44 | 45 | >>> import ZEO.ClientStorage 46 | >>> orig_blob_cache_layout_size = ZEO.ClientStorage.BlobCacheLayout.size 47 | >>> ZEO.ClientStorage.BlobCacheLayout.size = 11 48 | 49 | Now, let's write some data: 50 | 51 | >>> import ZODB.blob, transaction, time 52 | >>> conn = db.open() 53 | >>> for i in range(1, 101): 54 | ... conn.root()[i] = ZODB.blob.Blob() 55 | ... with conn.root()[i].open('w') as f: 56 | ... w = f.write((chr(i)*100).encode('ascii')) 57 | >>> transaction.commit() 58 | 59 | We've committed 10000 bytes of data, but our target size is 3000. We 60 | expect to have not much more than the target size in the cache blob 61 | directory. 62 | 63 | >>> import os 64 | >>> def cache_size(d): 65 | ... size = 0 66 | ... for base, dirs, files in os.walk(d): 67 | ... for f in files: 68 | ... if f.endswith('.blob'): 69 | ... try: 70 | ... size += os.stat(os.path.join(base, f)).st_size 71 | ... except OSError: 72 | ... if os.path.exists(os.path.join(base, f)): 73 | ... raise 74 | ... return size 75 | 76 | >>> def check(): 77 | ... return cache_size('blobs') < 5000 78 | >>> def onfail(): 79 | ... return cache_size('blobs') 80 | 81 | >>> from ZEO.tests.forker import wait_until 82 | >>> wait_until("size is reduced", check, 99, onfail) 83 | 84 | If we read all of the blobs, data will be downloaded again, as 85 | necessary, but the cache size will remain not much bigger than the 86 | target: 87 | 88 | >>> for i in range(1, 101): 89 | ... with conn.root()[i].open() as f: 90 | ... data = f.read() 91 | ... if data != (chr(i)*100).encode('ascii'): 92 | ... print('bad data', repr(chr(i)), repr(data)) 93 | 94 | >>> wait_until("size is reduced", check, 99, onfail) 95 | 96 | >>> for i in range(1, 101): 97 | ... with conn.root()[i].open() as f: 98 | ... data = f.read() 99 | ... if data != (chr(i)*100).encode('ascii'): 100 | ... print('bad data', repr(chr(i)), repr(data)) 101 | 102 | >>> for i in range(1, 101): 103 | ... with conn.root()[i].open('c') as f: 104 | ... data = f.read() 105 | ... if data != (chr(i)*100).encode('ascii'): 106 | ... print('bad data', repr(chr(i)), repr(data)) 107 | 108 | >>> wait_until("size is reduced", check, 99, onfail) 109 | 110 | Now let see if we can stress things a bit. We'll create many clients 111 | and get them to pound on the blobs all at once to see if we can 112 | provoke problems: 113 | 114 | >>> import threading, random 115 | >>> def run(): 116 | ... db = ZEO.DB(addr, blob_dir='blobs', blob_cache_size=4000) 117 | ... conn = db.open() 118 | ... for i in range(300): 119 | ... time.sleep(0) 120 | ... i = random.randint(1, 100) 121 | ... with conn.root()[i].open() as f: 122 | ... data = f.read() 123 | ... if data != (chr(i)*100).encode('ascii'): 124 | ... print('bad data', repr(chr(i)), repr(data)) 125 | ... i = random.randint(1, 100) 126 | ... with conn.root()[i].open('c') as f: 127 | ... data = f.read() 128 | ... if data != (chr(i)*100).encode('ascii'): 129 | ... print('bad data', repr(chr(i)), repr(data)) 130 | ... db.close() 131 | 132 | >>> threads = [threading.Thread(target=run) for i in range(10)] 133 | >>> for thread in threads: 134 | ... thread.daemon = True 135 | >>> for thread in threads: 136 | ... thread.start() 137 | >>> for thread in threads: 138 | ... thread.join(99) 139 | ... if thread.is_alive(): 140 | ... print("Can't join thread.") 141 | 142 | >>> wait_until("size is reduced", check, 99, onfail) 143 | 144 | .. cleanup 145 | 146 | >>> for thread in threading.enumerate(): 147 | ... if thread not in old_threads: 148 | ... thread.join(33) 149 | 150 | >>> db.close() 151 | >>> ZEO.ClientStorage.BlobCacheLayout.size = orig_blob_cache_layout_size 152 | -------------------------------------------------------------------------------- /src/ZEO/util.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2002 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Utilities for setting up the server environment.""" 15 | 16 | import os 17 | 18 | 19 | def parentdir(p, n=1): 20 | """Return the ancestor of p from n levels up.""" 21 | d = p 22 | while n: 23 | d = os.path.dirname(d) 24 | if not d or d == '.': 25 | d = os.getcwd() 26 | n -= 1 27 | return d 28 | 29 | 30 | class Environment: 31 | """Determine location of the Data.fs & ZEO_SERVER.pid files. 32 | 33 | Pass the argv[0] used to start ZEO to the constructor. 34 | 35 | Use the zeo_pid and fs attributes to get the filenames. 36 | """ 37 | 38 | def __init__(self, argv0): 39 | v = os.environ.get("INSTANCE_HOME") 40 | if v is None: 41 | # looking for a Zope/var directory assuming that this code 42 | # is installed in Zope/lib/python/ZEO 43 | p = parentdir(argv0, 4) 44 | if os.path.isdir(os.path.join(p, "var")): 45 | v = p 46 | else: 47 | v = os.getcwd() 48 | self.home = v 49 | self.var = os.path.join(v, "var") 50 | if not os.path.isdir(self.var): 51 | self.var = self.home 52 | 53 | pid = os.environ.get("ZEO_SERVER_PID") 54 | if pid is None: 55 | pid = os.path.join(self.var, "ZEO_SERVER.pid") 56 | 57 | self.zeo_pid = pid 58 | self.fs = os.path.join(self.var, "Data.fs") 59 | -------------------------------------------------------------------------------- /src/ZEO/version.txt: -------------------------------------------------------------------------------- 1 | 3.7.0b3 2 | -------------------------------------------------------------------------------- /src/ZEO/zconfig.py: -------------------------------------------------------------------------------- 1 | """SSL configuration support 2 | """ 3 | import os 4 | 5 | 6 | def ssl_config(section, server): 7 | import ssl 8 | 9 | cafile = capath = None 10 | auth = section.authenticate 11 | if auth: 12 | if os.path.isdir(auth): 13 | capath = auth 14 | elif auth != 'DYNAMIC': 15 | cafile = auth 16 | 17 | context = ssl.create_default_context( 18 | ssl.Purpose.CLIENT_AUTH if server else ssl.Purpose.SERVER_AUTH, 19 | cafile=cafile, capath=capath) 20 | 21 | if not auth: 22 | assert not server 23 | context.load_default_certs() 24 | 25 | if section.certificate: 26 | password = section.password_function 27 | if password: 28 | module, name = password.rsplit('.', 1) 29 | module = __import__(module, globals(), locals(), ['*'], 0) 30 | password = getattr(module, name) 31 | context.load_cert_chain(section.certificate, section.key, password) 32 | 33 | context.verify_mode = ssl.CERT_REQUIRED 34 | 35 | context.verify_flags |= ssl.VERIFY_X509_STRICT | ( 36 | context.cert_store_stats()['crl'] and ssl.VERIFY_CRL_CHECK_LEAF) 37 | 38 | if server: 39 | context.check_hostname = False 40 | return context 41 | 42 | context.check_hostname = section.check_hostname 43 | 44 | return context, section.server_hostname 45 | 46 | 47 | def server_ssl(section): 48 | return ssl_config(section, True) 49 | 50 | 51 | def client_ssl(section): 52 | return ssl_config(section, False) 53 | 54 | 55 | class ClientStorageConfig: 56 | 57 | def __init__(self, config): 58 | self.config = config 59 | self.name = config.getSectionName() 60 | 61 | def open(self): 62 | from ZEO.ClientStorage import ClientStorage 63 | 64 | # config.server is a multikey of socket-connection-address values 65 | # where the value is a socket family, address tuple. 66 | config = self.config 67 | 68 | addresses = [server.address for server in config.server] 69 | options = {} 70 | if config.blob_cache_size is not None: 71 | options['blob_cache_size'] = config.blob_cache_size 72 | if config.blob_cache_size_check is not None: 73 | options['blob_cache_size_check'] = config.blob_cache_size_check 74 | if config.client_label is not None: 75 | options['client_label'] = config.client_label 76 | 77 | ssl = config.ssl 78 | if ssl: 79 | options['ssl'] = ssl[0] 80 | options['ssl_server_hostname'] = ssl[1] 81 | 82 | return ClientStorage( 83 | addresses, 84 | blob_dir=config.blob_dir, 85 | shared_blob_dir=config.shared_blob_dir, 86 | storage=config.storage, 87 | cache_size=config.cache_size, 88 | cache=config.cache_path, 89 | name=config.name, 90 | read_only=config.read_only, 91 | read_only_fallback=config.read_only_fallback, 92 | server_sync=config.server_sync, 93 | wait_timeout=config.wait_timeout, 94 | **options) 95 | -------------------------------------------------------------------------------- /src/ZEO/zeoctl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.3 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2005 Zope Foundation and Contributors. 5 | # All Rights Reserved. 6 | # 7 | # This software is subject to the provisions of the Zope Public License, 8 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 9 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 10 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 11 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 12 | # FOR A PARTICULAR PURPOSE 13 | # 14 | ############################################################################## 15 | 16 | """Wrapper script for zdctl.py that causes it to use the ZEO schema.""" 17 | 18 | import os 19 | 20 | import zdaemon.zdctl 21 | 22 | import ZEO 23 | 24 | 25 | # Main program 26 | def main(args=None): 27 | options = zdaemon.zdctl.ZDCtlOptions() 28 | options.schemadir = os.path.dirname(ZEO.__file__) 29 | options.schemafile = "zeoctl.xml" 30 | zdaemon.zdctl.main(args, options) 31 | 32 | 33 | if __name__ == "__main__": 34 | main() 35 | -------------------------------------------------------------------------------- /src/ZEO/zeoctl.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This schema describes the configuration of the ZEO storage server 5 | controller. It differs from the schema for the storage server 6 | only in that the "runner" section is required. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 | 28 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | [tox] 4 | minversion = 3.18 5 | envlist = 6 | release-check 7 | lint 8 | py39 9 | py310 10 | py311 11 | py312 12 | py313 13 | pypy3 14 | docs 15 | coverage 16 | 17 | [testenv] 18 | usedevelop = true 19 | package = wheel 20 | wheel_build_env = .pkg 21 | deps = 22 | setuptools == 75.8.2 23 | !zodbmaster: ZODB >= 4.2.0b1 24 | zodbmaster: git+https://github.com/zopefoundation/ZODB.git@master\#egg=ZODB 25 | uvloop: uvloop 26 | setenv = 27 | msgpack1: ZEO_MSGPACK=1 28 | commands = 29 | # Run unit tests first. 30 | zope-testrunner -u --test-path=src -a 1000 {posargs:-vc} 31 | # Only run functional tests if unit tests pass. 32 | zope-testrunner -f --test-path=src -a 1000 {posargs:-vc} 33 | extras = 34 | test 35 | 36 | [testenv:setuptools-latest] 37 | basepython = python3 38 | deps = 39 | git+https://github.com/pypa/setuptools.git\#egg=setuptools 40 | !zodbmaster: ZODB >= 4.2.0b1 41 | zodbmaster: git+https://github.com/zopefoundation/ZODB.git@master\#egg=ZODB 42 | uvloop: uvloop 43 | 44 | [testenv:release-check] 45 | description = ensure that the distribution is ready to release 46 | basepython = python3 47 | skip_install = true 48 | deps = 49 | setuptools == 75.8.2 50 | wheel 51 | twine 52 | build 53 | check-manifest 54 | check-python-versions >= 0.20.0 55 | wheel 56 | commands_pre = 57 | commands = 58 | check-manifest 59 | check-python-versions --only setup.py,tox.ini,.github/workflows/tests.yml 60 | python -m build --sdist --no-isolation 61 | twine check dist/* 62 | 63 | [testenv:lint] 64 | description = This env runs all linters configured in .pre-commit-config.yaml 65 | basepython = python3 66 | skip_install = true 67 | deps = 68 | pre-commit 69 | commands_pre = 70 | commands = 71 | pre-commit run --all-files --show-diff-on-failure 72 | 73 | [testenv:docs] 74 | basepython = python3 75 | skip_install = false 76 | extras = 77 | docs 78 | commands_pre = 79 | commands = 80 | sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html 81 | 82 | [testenv:coverage] 83 | basepython = python3 84 | allowlist_externals = 85 | mkdir 86 | deps = 87 | coverage[toml] 88 | !zodbmaster: ZODB >= 4.2.0b1 89 | zodbmaster: git+https://github.com/zopefoundation/ZODB.git@master\#egg=ZODB 90 | uvloop: uvloop 91 | commands = 92 | mkdir -p {toxinidir}/parts/htmlcov 93 | coverage run -m zope.testrunner --test-path=src {posargs:-vc} 94 | coverage html 95 | coverage report 96 | --------------------------------------------------------------------------------