PhoenixmlDb
Reads During Writes
What a read, a query and a read transaction return while documents are written — one committed version per read, read transactions, limits on readers and running queries, and how the servers answer them
#Reads During Writes
Every read of a document returns one committed version of it. A query and a read transaction from
BeginRead read one committed state of the database. This page gives the rules, the limits on
readers and running queries, and the answers of the servers at those limits.
#What a read returns
A read of a document returns one committed version of it, whole: the content, the node tree, the metadata and the header values (id, size, timestamps, content type) are as one write left them. A write that commits while the read runs is not seen by that read.
|
While the read runs |
The read returns |
|---|---|
|
The document is replaced |
The old version or the new version; never parts of both |
|
The document is deleted |
The whole document, or "not found" |
|
The document is deleted and stored again |
The deleted document, "not found", or the new document |
A read never returns empty content for a document that has content, and never an internal error because of a write.
This holds for every read of a document:
|
Host |
Reads |
|---|---|
|
Embedded |
|
|
gRPC server |
|
|
REST server |
|
#A document in one call
ContainerReadExtensions (namespace PhoenixmlDb.Storage) has two reads on IContainer:
|
Method |
Returns |
|---|---|
|
|
A |
|
|
The content, or |
GetDocumentAsync followed by GetContentAsync is two reads: a document deleted between them is a
DocumentNotFoundException from the second (below).
With a cancelled CancellationToken, neither method reads anything. ReadDocumentAsync also checks
the token between the content and the metadata.
#A document object obtained earlier
An IDocument from IContainer.GetDocumentAsync is a handle on the document that has that name in
that container. Each read through it is one read as described in
What a read returns, of the version that is current when the read starts.
|
Since |
A read through the object returns |
|---|---|
|
replaced |
the replacement |
|
deleted |
|
|
deleted and stored again |
the new document |
|
Member |
Value |
|---|---|
|
|
Do not change |
|
|
Those of the header last read through the object: at first the header |
|
|
Content of the current version |
|
|
The root of the current version; its other nodes are read later, see Streams and node trees |
|
|
Metadata of the document that has the name at the time of the call |
After a content read, Modified, SizeBytes and Id describe the content that read returned. If
one of them differs from the value read with the header earlier, the content is of a later version.
DocumentDatabase.GetDocumentByIdAsync returns null for the id of a deleted document, also when
another document has been stored under its name.
#Read transactions
DocumentDatabase.BeginRead() returns a transaction that reads the committed state at the moment
BeginRead is called. Everything read through it is of that state, whatever is written afterwards:
-
GetDocumentAsync, and every read through the document it returns: content, stream, node tree, metadata. A document replaced afterBeginReadis read in the version ofBeginRead; a document deleted afterBeginReadis still returned, whole; a document stored afterBeginReadis not found. -
ListDocumentsAsync. -
QueryAsync: the documents, their content and their metadata (phx:metadata). A query in a read transaction does not use value indexes: it is evaluated on every document, and gives the answer it gives with them (Indexing: Value Index).
using (var txn = db.BeginRead())
{
// Both reads are of the state at BeginRead, whatever commits in between
IDocument? doc = await txn.GetDocumentAsync(products.Id, "p1.xml");
await foreach (var item in txn.QueryAsync(products.Id, "count(collection())"))
Console.WriteLine(item);
}
What an open read transaction costs:
-
It holds one of the database's reader slots (Limits on readers) from
BeginReaduntil it ends. -
While it is open, the space of every version written after
BeginReadis not reused. Each write takes new space in the database file, up to the map size (PhoenixmlDb:Storage:MapSizeMb); at the map size writes fail. The space is reused after the transaction ends.
A read transaction ends when:
-
it is disposed;
-
its maximum lifetime is over. There is none unless one is set:
DocumentDatabase.BeginRead(TimeSpan maxLifetime)for one transaction,LmdbStorageOptions.ReadTransactionMaxLifetime/PhoenixmlDb:Storage:ReadTransactionMaxLifetimeSecondsfor every transaction of the database; -
its database is disposed.
DisposeandDisposeAsyncof the database end every read transaction that is still open and the read transaction of every query that is still running (Queries), after a read that is running through one has completed. When they return, the database directory can be opened again.
After it has ended, the transaction and every document obtained from it raise
ObjectDisposedException; the text says which of the three ended it. A transaction may be used
from several threads; its reads run one at a time.
The number of read transactions open at one moment is limited (Limits on readers).
A read through a write transaction (BeginWriteAsync) reads the committed state at the moment of
that read, as one read (What a read returns).
#Streams and node trees
GetContentStreamAsync reads the whole content and then returns. The stream is of one version
whenever it is read, and it holds no transaction: a stream that is not disposed holds no reader
slot. The memory it holds is the UTF-8 bytes of the document.
GetRootNodeAsync reads the header and the document node. The other nodes are read when they are
asked for: Parent reads one node, StringValue reads the descendants of the node, each as one
read. No tree is kept in memory.
The nodes are those of the version the root came from, and never of another:
|
Since |
|
|---|---|
|
not written |
answer |
|
replaced, also by the same content |
|
|
deleted and stored again |
|
|
deleted |
|
A node keeps a StringValue it has read once. A root obtained again reads the document as it is at
that time. A root from a document of a read transaction (Read transactions)
reads through the transaction and is not affected by writes.
The engine has no limit on the size of a document. The servers have: the REST server stores
documents up to PhoenixmlDb:Documents:MaxDocumentSize (default 10 MB), and the gRPC server
receives messages up to 100 MB.
#Queries
A query reads through one read transaction, opened when the query starts. The documents it lists,
the entries of a value index that name its candidates, every document it loads (its own and those
doc() names), their metadata (phx:metadata) and the statistics it is planned with are of the
state of the container at that moment. This holds for a query that is evaluated once per document
and for one that is evaluated once over the container (fn:collection() with an aggregate,
order by, group by, distinct-values); see
Once per document or once over the container.
-
A document replaced or deleted while the query runs is answered in the version the query started with.
-
A document stored after the query started is not visited.
-
A container that held no document when the query started answers nothing.
-
An answer with a value index is the answer without it, also while documents are written.
A query runs from its start, the first MoveNextAsync, until its results are enumerated to the
end, its enumerator is disposed (leaving an await foreach early does that), it is cancelled or it
fails. While it runs it holds one reader slot (Limits on readers), and no
second one. The cost of an open read transaction (Read transactions) applies
for as long as the query is enumerated.
The number of queries running at one moment is limited (MaxConcurrentQueries). A query that
starts at the limit, or when no reader slot is free, fails at its start with ReaderLimitException.
It does not wait for a place.
A database that is disposed ends every query that is still running: the next MoveNextAsync of
such a query raises ObjectDisposedException ("The query ended: its database was disposed."), also
for an enumerator that is never disposed.
A query in a read transaction reads through the transaction, holds no slot of its own and is not counted as a running query of the database.
#Time limits
|
Host |
The longest a query is enumerated |
|---|---|
|
REST server |
The request's |
|
gRPC server |
The request's |
|
Embedded |
No limit: until the caller ends the enumeration, or cancels through the |
The limit covers the whole call: for a streamed answer it includes the time the client takes to read the results.
|
Host |
A query that ran for its limit |
|---|---|
|
REST server |
|
|
gRPC server |
Status |
#Running queries per API key
Both servers limit the queries one API key has running at one moment
(PhoenixmlDb:Query:MaxConcurrentQueriesPerApiKey, default 16; all callers together on a server
without keys). One more does not start and does not wait:
|
Host |
Calls counted |
Answer at the limit |
|---|---|---|
|
gRPC server |
|
Status |
|
REST server |
|
|
The REST server limits transformations separately (PhoenixmlDb:Transform:MaxConcurrentTransforms,
for the server, 503 TRANSFORMS_UNAVAILABLE); a transformation is not a running query.
#Limits on readers
A database has MaxReaders reader slots. Every read transaction that is open holds one: a read of
a document or of metadata (for the time of the read), a running query (Queries), a
backup, an open read transaction (Read transactions).
|
Setting |
Default |
Limit |
|---|---|---|
|
|
126 |
Read transactions of every kind open at one moment |
|
|
a quarter of |
Read transactions from |
|
|
half of |
Queries running at one moment. At least 1, and at most |
|
|
unset: no limit |
The longest a read transaction from |
With the defaults, read transactions and running queries together hold at most 31 + 63 = 94 of the 126 reader slots. 32 are left for the reads that hold a slot for the time of one read: document and metadata reads in progress, a backup, the full-text indexer.
A read that cannot begin because a limit is reached fails with ReaderLimitException
(a PhoenixmlDbStorageException; SettingName, Limit). Nothing is read and nothing is changed;
the read succeeds when a reader has ended.
|
Limit reached |
Who is refused |
|---|---|
|
|
Any read: a document read, a listing, a query, |
|
|
|
|
|
A query, at its start. Document reads, writes, |
|
Host |
|
|---|---|
|
gRPC server |
Status |
|
REST server |
|
#REST server: a document deleted during a request
A request that finds a document and then reads it again after it has been deleted answers 404
with errorCode NOT_FOUND and resourceType Document. The answer carries no document name.
#gRPC server: transactions
The transactions of TransactionService.BeginTransaction, read-only and read-write, have limits
set under PhoenixmlDb:Transactions (LeaseSeconds, MaxLifetimeSeconds, MaxOpen,
MaxOpenPerApiKey; see
Server Configuration: Transactions).
-
A transaction that no call has used for
LeaseSecondsis rolled back. A use is a call that names the transaction: a write, a commit, a rollback.GetTransactionStatusis not a use. -
A transaction that has been open for
MaxLifetimeSecondsis rolled back, used or not. -
One more
BeginTransactionatMaxOpenorMaxOpenPerApiKeyis answeredResourceExhausted, and nothing is begun for it: a refused read-write transaction does not wait for the write lock. -
A transaction whose lease or lifetime ends while a call is in flight on it is rolled back when that call has returned; the call completes as it would have. Calls that name the transaction afterwards are answered that it is not open.
A read-only transaction holds no reader slot and keeps no space from being reused while no call
reads through it. No gRPC call reads through a read-only transaction: GetDocument,
ListDocuments and the query calls read the current state, each as described in
What a read returns and Queries.
#Next Steps
-
Transactions: write transactions and what is atomic
-
Documents & Storage: storing and retrieving documents
-
Server Configuration: every server setting