跳转到主要内容

查看指定时间之前的存储

项目描述

https://github.com/zopefoundation/zc.beforestorage/workflows/tests/badge.svg

存储之前

ZODB 存储通常存储多个对象版本,以支持多版本并发控制和撤销等特性。在 mod 流行存储实现中,旧版本在打包前不会被丢弃。这个特性经常被用来进行时间旅行,允许用户查看数据库在某个时间点的状态。在过去,通过指定打开文件存储的时间,可以实现文件存储的时间旅行。这对于大数据库来说速度非常慢,因为现有的索引文件很难使用。ZODB 历史机制也支持通过 loadBefore 方法对单个对象进行时间旅行。

多版本并发控制的出现为时间旅行提供了新的机会。使用存储的 loadBefore 方法,可以加载给定时间之前的交易记录。ZODB 3.9 将为数据库 open 方法提供选项,以便在某个时间点打开连接。

演示存储对于测试非常有用,尤其是对于部署应用。在常见的配置中,它们允许在不更改底层数据库的情况下存储对基本数据库的更改。Zope 功能测试框架利用演示存储在测试后轻松将数据库状态回滚到测试前的非空状态。演示存储的一个显著限制是,它们不能与变化的底层存储一起使用。这意味着它们通常不能与 ZEO 一起使用。如果底层数据库仍然被其他客户端更改,则仅拥有只读连接是不够的。

“before”存储提供了另一种利用 loadBefore 方法进行时间旅行和支持向 ZEO 服务器提供不变视图的方法。一个 before 存储是一个数据库适配器,它提供了一个特定时间点底层存储的只读视图。

使用 ZConfig 配置 Before 存储

“before”选项

要使用 ZConfig 配置文件中的 before 存储,需要导入 zc.beforestorage,然后使用 before 存储部分。

>>> import ZODB.config
>>> storage = ZODB.config.storageFromString("""
...
... %import zc.beforestorage
...
... <before>
...     before 2008-01-21
...     <filestorage>
...         path my.fs
...     </filestorage>
... </before>
... """)
>>> storage
<Before: my.fs before 2008-01-21 00:00:00.000000>
>>> storage.close()

如果我们省略 before 选项,我们将使用当前时间。

>>> storage = ZODB.config.storageFromString("""
...
... %import zc.beforestorage
...
... <before>
...     <filestorage>
...         path my.fs
...     </filestorage>
... </before>
... """)
>>> storage
<Before: my.fs before 2008-01-21 18:22:49.000000>
>>> storage.close()

我们也可以给出选项 'now' 并获取当前时间。

>>> import ZODB.config
>>> storage = ZODB.config.storageFromString("""
...
... %import zc.beforestorage
...
... <before>
...     before now
...     <filestorage>
...         path my.fs
...     </filestorage>
... </before>
... """)
>>> storage
<Before: my.fs before 2008-01-21 18:22:53.000000>
>>> storage.close()

我们可以给出选项 'startup' 并获取启动时间。

>>> import ZODB.config
>>> storage = ZODB.config.storageFromString("""
...
... %import zc.beforestorage
...
... <before>
...     before startup
...     <filestorage>
...         path my.fs
...     </filestorage>
... </before>
... """)
>>> storage
<Before: my.fs before 2008-01-21 18:22:43.000000>
>>> import zc.beforestorage
>>> import ZODB.TimeStamp
>>> print(
...     str(zc.beforestorage.startup_time_stamp))
2008-01-21 18:22:43.000000
>>> storage.close()

“before-from-file”选项

“before-from-file”选项可以用来在重启之间保留更改文件。它的值是文件的绝对路径。如果文件存在,将从中读取“before”时间。如果文件不存在,则将创建该文件并将当前的 UTC 时间写入其中。

当与未设置“create=true”选项的更改文件一起使用时,数据库将在重启之间保留。

>>> import os.path
>>> import tempfile
>>> tempdir = tempfile.mkdtemp()
>>> before_file = os.path.join(tempdir, 'before-file')

当前文件不存在。所以它将被创建并使用当前时间写入。为了使这可重复,我们“monkeypatch”模块中的“get_now”函数以返回一个固定值。

>>> import datetime
>>> import zc.beforestorage
>>> def fake_get_utcnow():
...     return datetime.datetime(2008, 1, 1, 15, 0)
>>> orig_get_utcnow = zc.beforestorage.get_utcnow
>>> zc.beforestorage.get_utcnow = fake_get_utcnow
>>> os.path.exists(before_file)
False
>>> storage = ZODB.config.storageFromString("""
...
... %%import zc.beforestorage
...
... <before>
...     before-from-file %s
...     <filestorage>
...         path my.fs
...     </filestorage>
... </before>
... """ % before_file)
>>> storage
<Before: my.fs before 2008-01-01 15:00:00.000000>
>>> storage.close()

现在文件已经被创建。

>>> os.path.exists(before_file)
True
>>> f = open(before_file)
>>> f.read() == fake_get_utcnow().replace(microsecond=0).isoformat()
True

如果我们现在向文件写入新值,存储将使用该时间启动。

>>> f = open(before_file, 'w')
>>> _ = f.write('1990-01-01T11:11')
>>> f.close()
>>> storage = ZODB.config.storageFromString("""
...
... %%import zc.beforestorage
...
... <before>
...     before-from-file %s
...     <filestorage>
...         path my.fs
...     </filestorage>
... </before>
... """ % before_file)
>>> storage
<Before: my.fs before 1990-01-01 11:11:00.000000>
>>> storage.close()

如果我们重新启动存储,将使用文件中的值。

>>> storage = ZODB.config.storageFromString("""
...
... %%import zc.beforestorage
...
... <before>
...     before-from-file %s
...     <filestorage>
...         path my.fs
...     </filestorage>
... </before>
... """ % before_file)
>>> storage
<Before: my.fs before 1990-01-01 11:11:00.000000>
>>> storage.close()

这将一直发生,直到我们删除文件。before_from_file 路径存储在存储本身上,因此使用它的应用程序可以访问它。

>>> os.remove(storage.before_from_file)
>>> os.path.exists(before_file)
False

如果我们再次重新启动存储,将创建一个新的文件。

>>> storage = ZODB.config.storageFromString("""
...
... %%import zc.beforestorage
...
... <before>
...     before-from-file %s
...     <filestorage>
...         path my.fs
...     </filestorage>
... </before>
... """ % before_file)
>>> storage
<Before: my.fs before 2008-01-01 15:00:00.000000>
>>> storage.close()

注意,与“before”选项不同,“before-from-file”文件不能包含“now”或“startup”等特殊值。

>>> f = open(before_file, 'w')
>>> _ = f.write('now')
>>> f.close()
>>> storage = ZODB.config.storageFromString("""
...
... %%import zc.beforestorage
...
... <before>
...     before-from-file %s
...     <filestorage>
...         path my.fs
...     </filestorage>
... </before>
... """ % before_file)
Traceback (most recent call last):
...
ValueError: 8-byte array expected

注意,只能指定“before”或“before-from-file”选项中的一个,不能同时指定两个。

>>> storage = ZODB.config.storageFromString("""
...
... %%import zc.beforestorage
...
... <before>
...     before 2008-01-01
...     before-from-file %s
...     <filestorage>
...         path my.fs
...     </filestorage>
... </before>
... """ % before_file)
Traceback (most recent call last):
  ...
ValueError: Only one of "before" or "before-from-file" options can be specified, not both

清理...

>>> import shutil
>>> shutil.rmtree(tempdir)
>>> zc.beforestorage.get_utcnow = orig_get_utcnow

演示(doctest)

请注意,大多数人将通过 ZConfig 配置存储。如果你是这些人之一,你可能想在这里停止。 :) 下面的示例展示了如何从 Python 使用存储,但它们也练习了你可能不感兴趣的大量细节。

为了在Python层面了解其工作原理,我们将创建一个文件存储,并使用一个“之前存储”来提供对其的视图。

>>> import ZODB.FileStorage
>>> fs = ZODB.FileStorage.FileStorage('Data.fs')
>>> from ZODB.DB import DB
>>> db = DB(fs)
>>> conn = db.open()
>>> root = conn.root()
>>> import persistent.mapping

我们将记录事务标识符,我们在打开“之前存储”时将使用这些标识符。

>>> import transaction
>>> transactions = [root._p_serial]
>>> for i in range(1, 11):
...     root[i] = persistent.mapping.PersistentMapping()
...     transaction.get().note("trans %s" % i)
...     transaction.commit()
...     transactions.append(root._p_serial)

通过调用具有现有存储和时间的“Before”构造函数来创建一个“之前存储”。

>>> import zc.beforestorage
>>> b5 = zc.beforestorage.Before(fs, transactions[5])
>>> db5 = DB(b5)
>>> conn5 = db5.open()
>>> root5 = conn5.root()
>>> len(root5)
4

在这里,我们看到的是在第5个事务提交之前的数据库状态。如果我们尝试访问更晚的对象,我们将得到一个“ReadConflictError”。

>>> conn5.get(root[5]._p_oid)
Traceback (most recent call last):
...
ZODB.POSException.ReadConflictError: b'\x00\x00\x00\x00\x00\x00\x00\x05'

类似地,尽管我们可以访问较早的对象版本,但我们无法访问“之前时间”或之后的时间版本。

>>> _ = b5.loadSerial(root._p_oid, transactions[2])
>>> b5.loadSerial(root._p_oid, transactions[5])
Traceback (most recent call last):
...
POSKeyError: 0x00

让我们通过存储方法来了解一下。

>>> (b5.getName() ==
...  'Data.fs before %s' % ZODB.TimeStamp.TimeStamp(transactions[5]))
True
>>> b5.getSize() == fs.getSize()
True
>>> for hd in b5.history(root._p_oid, size=3):
...     print(hd['description'].decode('utf-8'))
trans 4
trans 3
trans 2
>>> b5.isReadOnly()
True
>>> transactions[4] <= b5.lastTransaction() < transactions[5]
True
>>> len(b5) == len(fs)
True
>>> p, s1, s2 = b5.loadBefore(root._p_oid, transactions[5])
>>> p == fs.loadSerial(root._p_oid, transactions[4])
True
>>> s1 == transactions[4]
True
>>> s2 is None
True
>>> p, s1, s2 = b5.loadBefore(root._p_oid, transactions[4])
>>> p == fs.loadSerial(root._p_oid, transactions[3])
True
>>> s1 == transactions[3]
True
>>> s2 == transactions[4]
True
>>> b5.getTid(root._p_oid) == transactions[4]
True
>>> b5.tpc_transaction()
>>> try:
...     b5.new_oid()
... except Exception as e: # Workaround http://bugs.python.org/issue19138
...     print(e.__class__.__name__)
ReadOnlyError
>>> from ZODB.TimeStamp import TimeStamp
>>> try:
...     b5.pack(TimeStamp(transactions[3]).timeTime(), lambda p: [])
... except Exception as e:
...     print(e.__class__.__name__)
ReadOnlyError
>>> b5.registerDB(db5)
>>> b5.sortKey() == fs.sortKey()
True
>>> try:
...     b5.tpc_begin(transaction.get())
... except Exception as e:
...     print(e.__class__.__name__)
ReadOnlyError
>>> b5.store(root._p_oid, transactions[4], b5.load(root._p_oid)[0], '',
...          transaction.get())
... # doctest: +ELLIPSIS
Traceback (most recent call last):
...
StorageTransactionError: ...
>>> b5.tpc_vote(transaction.get())
... # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ZODB.POSException.StorageTransactionError: ...
>>> b5.tpc_finish(transaction)
... # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ZODB.POSException.StorageTransactionError: ...
>>> b5.tpc_transaction()
>>> b5.tpc_abort(transaction)

“之前存储”不支持撤销操作。

>>> b5.supportsUndo
Traceback (most recent call last):
...
AttributeError: 'Before' object has no attribute 'supportsUndo'

(不要问关于版本的事情。:)

关闭“之前存储”将关闭其下层的存储。

>>> b5.close()
>>> fs.load(root._p_oid, '') # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: ...

如果我们在创建“之前存储”时省略时间戳,将使用当前时间。

>>> fs = ZODB.FileStorage.FileStorage('Data.fs')
>>> from ZODB.DB import DB
>>> db = DB(fs)
>>> conn = db.open()
>>> root = conn.root()
>>> bnow = zc.beforestorage.Before(fs)
>>> dbnow = DB(bnow)
>>> connnow = dbnow.open()
>>> rootnow = connnow.root()
>>> for i in range(1, 11):
...     root[i] = persistent.mapping.PersistentMapping()
...     transaction.get().note("trans %s" % i)
...     transaction.commit()
...     transactions.append(root._p_serial)
>>> len(rootnow)
10
>>> dbnow.close()

时间戳可以直接传递,也可以作为ISO时间。例如

>>> fs = ZODB.FileStorage.FileStorage('Data.fs')
>>> iso = 'T'.join(str(ZODB.TimeStamp.TimeStamp(transactions[5])).split()
...                )[:19]
>>> b5 = zc.beforestorage.Before(fs, iso)
>>> db5 = DB(b5)
>>> conn5 = db5.open()
>>> root5 = conn5.root()
>>> len(root5)
4
>>> b5.close()

Blob 支持

如果包装的存储支持blobs,则“之前存储”支持blobs,并且实际上它只是暴露了底层存储的loadBlob和temporaryDirectory方法。

>>> fs = ZODB.FileStorage.FileStorage('Data.fs')
>>> import ZODB.blob
>>> bs = ZODB.blob.BlobStorage('blobs', fs)
>>> db = ZODB.DB(bs)
>>> conn = db.open()
>>> conn.root()['blob'] = ZODB.blob.Blob()
>>> _ = conn.root()['blob'].open('w').write(b'data1')
>>> transaction.commit()
>>> bnow = zc.beforestorage.Before(bs)
>>> dbnow = DB(bnow)
>>> connnow = dbnow.open()
>>> rootnow = connnow.root()
>>> _ = conn.root()['blob'].open('w').write(b'data2')
>>> transaction.commit()
>>> print(rootnow['blob'].open().read().decode('utf-8'))
data1
>>> bnow.temporaryDirectory() == bs.temporaryDirectory()
True
>>> import ZODB.interfaces, zope.interface.verify
>>> zope.interface.verify.verifyObject(
...     ZODB.interfaces.IBlobStorage, bnow)
True
>>> bnow.close()

更改

1.0 (2023-02-09)

  • 支持Python 3.9、3.10和3.11。

  • 不支持Python 2.7、3.5和3.6。

0.6 (2020-05-14)

  • 支持Python 3.5至3.8。

  • 不支持Python 3.3和3.4。

  • 修复了长期存在的loadBefore的bug。该bug通过针对ZODB 5进行测试而被揭示,对于ZODB 5,loadBefore起着更大的作用。

0.5.1 (2013-10-25)

  • 修复了损坏的发布版本。

0.5.0 (2013-10-25)

添加了ZODB4和Python 3支持。

0.4.0 (2010-12-09)

添加了“before-from-file”选项,如果应用程序希望在应用程序重启之间保留beforestorage状态,则可以使用此选项。

0.3.2 (2008-12-05)

更新以支持ZODB 3.8和3.9。

0.3.1 (2008-12-01)

将lastTid重命名为getTid,以符合ZEO.interfaces.IServeable接口。

0.3.0 (2008-12-01)

添加了对Blob的支持。

0.2.0 (2008-03-05)

当使用ZConfig时,添加了对“now”和“startup”值的支持。其中,“now”值表示“之前存储”应该提供自存储创建时起的基础存储视图。而“startup”值表示“之前存储”应该提供自进程启动时起的基础存储视图。后者在单个应用程序中设置多个“之前存储”时特别有用,因为它允许您安排所有存储提供一致的观点,而无需指定时间。

0.1.1 (2008-02-07)

修复了导致某些文件被省略的打包错误。

0.1 (2008-01-??)

初始发布。

项目详情


下载文件

下载适合您平台的文件。如果您不确定选择哪一个,请了解更多关于安装包的信息。

源分布

zc.beforestorage-1.0.tar.gz (18.2 kB 查看散列)

上传时间

构建分布

zc.beforestorage-1.0-py3-none-any.whl (14.9 kB 查看散列)

上传时间 Python 3

支持