The Build-and-Commit model is based on explicitly creating a transaction, invoking operations in the scope of this transaction and, finally, committing or closing the transaction. The transaction represents a session between client and UniConfig instance.
Using explicitly created transactions has multiple advantages in comparison to the Immediate Commit Model:
Multiple operations and modifications can be invoked in a single transaction while keeping transactions isolated.
Most UniConfig operations, such as calculate-diff and commit, have no use in the Immediate Commit Model. They are valuable only if the Build-and-Commit Model is used.
The transaction allows a client to identify if it still communicates with the same UniConfig instance (this property is usable in the clustered deployment). If the UniConfig instance does not know about the transaction, the request fails because the transaction expired, is closed or was never created in the first place.
Configurations related to UniConfig transactions are placed in the config/application.properties file under the transactions container.
# Grouped settings that are related to UniConfig transactions# Time after transaction can be closed [seconds] by transaction cleaner.transactions.transaction-idle-time-out=300# Maximum transaction age before it can be evicted from transaction registry [seconds].# Configuring '0' disables cleaning of UniConfig transactions.transactions.max-transaction-age=1800# Interval at which expired transactions are closed and cleaned [seconds].# Expired transaction: transaction which age exceeds 'maxTransactionAge' setting.# Only dedicated UniConfig transactions (initialized using 'create-transaction' RPC)# are cleaned - shared transaction is never removed or invalidated.# Configuring '0' disables cleaning of UniConfig transactions.transactions.cleaning-interval=60# Boolean value if the Immediate Commit Model is enabled or not. Default value is true.# If disabled, only manually created transactions can exist.transactions.immediate-commit-enabled=true
Race conditions between transactions that are committed in parallel and contain changes to the same nodes (uniconfig, unistore, snapshot or template nodes) are solved using the optimistic locking mechanism. The configuration of a node can be modified in parallel from two transactions, but only the first committed transaction will succeed. The commit for the second transaction will fail.
UniConfig uses two different techniques to detect conflicts during commit or checked-commit operations:
Comparing configuration fingerprints - The fingerprint value for an altered node is updated at the end of the commit operation. At the beginning of a commit operation, UniConfig compares the value of the actual fingerprint in the database with the value of the fingerprint read before the first CRUD operation done in the transaction and the last synced fingerprint (updated after execution of the sync-from-network RPC). If the actual fingerprint in the database is equal to the fingerprint read before the first CRUD operation or the last synced fingerprint, the commit operation can continue. Otherwise, an error is returned without touching any devices in the network.
Per-node advisory locks - Comparison of configuration fingerprints is reliable if transactions are committed one after another. However, such serialization cannot be achieved in the clustered environment as UniConfig instances are not coordinated. If two transactions are committed at the same time and both assume that configuration fingerprints haven't been updated by another transaction, both transactions may start to push changes to network devices simultaneously. To prevent this scenario, UniConfig locks the node in the PostgresSQL database using transaction-level advisory locks at the beginning of the commit operation. If another transaction tries to lock the same node, its attempt will fail, and the second transaction cannot enter the critical section but will fail. Locks are automatically released at the end of the transaction (commit RPC closes the transaction).
All possible scenarios are captured in the following diagrams.
Mountpoints are created only when UniConfig needs to read / write some data from / to device and lifecycle of mountpoint is bounded by lifecycles of transactions that use the mountpoint. If some mountpoint is not used by any transaction, then UniConfig automatically closes this mountpoint - associated operational data on southbound layer and connection to device are removed.
The first diagram demonstrates mounting of 2 devices which are used by 1 transaction - after this transaction is closed, both mountpoints are closed. The second diagram shows scenario in which 2 transactions share 1 of 2 mountpoints - after the first transaction is closed, 1 of the mountpoints is not closed since the second transaction still may communicate with corresponding device.
Transaction can be created using create-transaction RPC. RPC doesn't specify input body and also returns response without body. Response additionally contains Set-Cookie header with UNICONFIGTXID key and corresponding value - transaction identifier that conforms RFC-4122 Universally Unique IDentifier (UUID) format.
Process of transaction creation is depicted by following sequence diagram.
create-transaction RPC
UniConfig is performing following steps after calling create-transaction RPC:
Creation of connection to database system - Connection is created with disabled auto-commit - enabling transactional features. UniConfig uses 'read committed' isolation level.
Creation of database transaction - It provides access to remote PostgreSQL database. Using database transaction it is possible to read committed data, read uncommitted changes created by this transaction and write modifications to database. Data read at the first access to some resource is cached to datastore transaction - when some component tries to access the same resource again, it is read only from datastore transaction. Data is written to database transaction at invocation of commit/checked-commit RPC.
Creation of datastore read-write transaction - It provides access to OPER and CONFIG datastores bound to this transaction. Datastore is used only as a cache between application and PostgreSQL database, and it resides only in the memory allocated to UniConfig process. Datastore transaction is never committed - cache is trashed at the end of the transaction life.
Registration of transaction - Transaction is always bound to 1 specific UniConfig instance.
The most common reason for failed creation of UniConfig transaction is reached maximum number of open transactions that is limited by ('maxDbPoolSize' - 'maxInternalDbConnections') database connection pool setting. In that case, UniConfig returns response with 500 status code.
RPC Request
curl --location --request POST 'http://localhost:8181/rests/operations/uniconfig-manager:create-transaction'\
--header 'Accept: application/json'
RPC Response, Status: 500
{"errors":{"error":[{"error-type":"protocol","error-message":"Failed to create Uniconfig transaction","error-tag":"operation-failed","error-info":"Maximum open transactions created by User was reached"}]}}
Create-transaction RPC can be used with optional query parameter called timeout. This parameter is used to override global idle timeout for transaction created by this RPC call. After transaction inactivity for specified time transaction will be automatically cleaned. Value of this parameter is whole number and defines time in seconds.
By default, UniConfig shares a southbound session to a network device if multiple UniConfig transactions use the same device via the same management protocol. This behaviour can be disabled using the dedicatedDeviceSession query parameter, which accepts a boolean value. Afterwards the UniConfig transaction will create a dedicated session to the device, which is used only by one transaction and is closed immediately after committing or closing the transaction.
Dedicated sessions to a device are useful when:
The evice is not able to process requests in parallel via the same session.
The device is able to process requests in parallel via same session, but does not process them in parallel, decreasing processing performance.
CRUD operations for modification or reading node configuration can be invoked in the specific transaction by appending UNICONFIGTXID (key) with UUID of transaction (value) to Cookie headers. In that case, operation will be invoked only in the scope of single transaction - changes are not visible to other transactions until this transaction is successfully committed.
Next diagram describes execution of CRUD operation from RESTCONF API. It shows also difference between datastore and database transaction - data is read from database only at the first access to some data (for example, node configuration). After that, this configuration is cached inside temporary datastore transaction - goal is to improve performance by limiting transferring data between UniConfig and PostgreSQL. Next access to same configuration can be evaluated under in-memory datastore.
The following request demonstrates reading of some configuration from uniconfig topology, junos node in the transaction with ID 'd7ff736e-8efa-4cc5-9d27-b7f560a76ff3'.
RPC operation can be invoked in the specific transaction the same way as CRUD operation - by specification of UNICONFIGTXID in the Cookie header.
There are few differences between CRUD and RPC operations from the view of transactions:
Commit, checked-commit, and close-transaction RPCs can state of the transaction. Create-transaction RPC is reserved for creation of transaction.
Not all RPC operations that are exposed by UniConfig use dedicated transactions - in that case, these RPCs just ignore explicitly specified transaction and either don't work with transactions at all or create transaction internally (examples: install-node, uninstall-node RPC).
There are also transaction-aware operations that directly leverage properties of transactions. For example, if some UniConfig RPC is invoked with empty list of target nodes, then operation is automatically applied to all modified nodes in the transaction (calculate-diff RPC with empty target nodes computes diff for all modified nodes in the transaction).
Following diagram shows execution of random RPC in the specified transaction.
{"errors":{"error":[{"error-type":"protocol","error-message":"Invalid transaction-id format; it should conform UUID based on RFC 4122: 2a335e04-ae11-4677-885b-cea1ea71157x","error-tag":"access-denied","error-info":"Error at index 11 in: \"cea1ea71157x\""}]}}
There are 2 options how transaction can be closed:
close-transaction RPC - Explicit closing of transaction that results in dropping of all changes done in the transaction.
commit/checked-commit RPC - After execution of commit operation, transaction is automatically closed (despite of commit result). Behaviour of commit and checked commit RPC is described in better detail under the 'UniConfig Node Manager' section.
Close-transaction RPC doesn't contain body, only Cookie header with UNICONFIGTXID property pointing to transaction that user would like to close. Response contains information if transaction has been successfully closed.
Following sequence diagrams describe close-transaction procedure. It is split into 2 diagrams to improve readability and to reuse some parts from other diagrams.
close-transaction RPCClean orphaned mountpoints
Briefly depicted most important actions:
Loading UniConfig transaction from registry by provided transaction ID that is extracted from Cookie header.
Closing connection to database.
Cancellation of database transaction.
Cancellation of datastore read-write transaction.
Unregistration of transaction from local registry.
Unmounting nodes that are not referenced by any UniConfig transaction - connection to device is closed and representing southbound / Unified mountpoints are removed together with state data.
After transaction is closed, it cannot be used by any other operation - user must create a new transaction in order to use build-and-commit model.
{"errors":{"error":[{"error-message":"Uniconfig transaction 3819fbaa-6bd4-4c79-bc4e-68f70cf97903 has already been closed","error-tag":"invalid-transaction","error-type":"protocol"}]}}
Transaction cleaner is used for automatic closing of transactions that are open longer then specified timeout value ('transactionIdleTimeOut' or 'maxTransactionAge' setting in the configuration). Transaction resets her time setting 'transactionIdleTimeOut' after invoking CRUD, RPC operation, and is still valid for time specified in value of setting. This mechanism effectively suppresses application-level errors - open transactions are not closed at the end of the workflow.
Next sequence diagram describes cleaning process. Referenced diagram 'Close transaction' is placed in the previous 'Closing transaction' section.
Both responses should return Status 404 Not Found:
RPC Response, Status: 404
{"errors":{"error":[{"error-message":"Request could not be completed because the relevant data model content does not exist","error-tag":"data-missing","error-type":"protocol"}]}}
6. Committing TX1 and TX2 using uniconfig-manager:commit RPC
It is not required to specify target nodes in the input because UniConfig transaction tracks modified nodes:
Since there aren't any conflicts between modifications in the committed transactions, both RPCs should succeed. Expected responses:
RPC Response, Status: 204
RPC Response, Status: 204
7. Verification of committed data
Verification if configuration was correctly committed to devices (direct read under yang-ext:mount) and if datastore was updated (GET request without transaction ID):
{"errors":{"error":[{"error-message":"Uniconfig transaction 5e8ab9d0-803a-40d6-9f0a-92e47524bab8 has already been closed","error-tag":"invalid-transaction","error-type":"protocol"}]}}
{"errors":{"error":[{"error-message":"Uniconfig transaction 73f85310-a20a-46b9-beaf-d2ac98cc74cc has already been closed","error-tag":"invalid-transaction","error-type":"protocol"}]}}
#Modification of sub-tree on same device in separate transactions
5. Modification of ‘xr6_1’ uniconfig configuration inside TX2
Changing description of interface Loopback97 to 'next loopback': - there is a conflict with TX1 which also tries to create/replace the configuration of the same interface:
Commit TX1 without target nodes - it should fail because the same node has already been modified by different transaction that has already been committed:
{"errors":{"error":[{"error-tag":"validation-failed","error-type":"application","error-message":"TransactionCommitFailedException{message=\"14804ad7-c0f6-4b22-8336-7484efc23e31: Node 'xr6_1' in topology 'uniconfig' has been modified by other transactions."}]}}
8. Verification of committed data in TX1 / non-committed data in TX2
Verification if committed changes in TX1 were applied to datastore and device:
RPC Request
curl --location --request GET 'http://localhost:8181/rests/data/network-topology:network-topology/topology=uniconfig/node=xr6_1/frinx-uniconfig-topology:configuration/Cisco-IOS-XR-ifmgr-cfg:interface-configurations/interface-configuration=act,Loopback97?content=config'\
--header 'Content-Type: application/json'\
--header 'Accept: application/json'\
--header 'Authorization: Basic YWRtaW46YWRtaW4='\
--header 'Cookie: JSESSIONID=node0ovyof0o58vgp5868y4028wd216.node0'curl --location --request GET 'http://localhost:8181/rests/data/network-topology:network-topology/topology=uniconfig/node=xr6_1/frinx-uniconfig-topology:configuration/Cisco-IOS-XR-ifmgr-cfg:interface-configurations/interface-configuration=act,Loopback97?content=nonconfig'\
--header 'Content-Type: application/json'\
--header 'Accept: application/json'\
--header 'Authorization: Basic YWRtaW46YWRtaW4='\
--header 'Cookie: JSESSIONID=node0cxljma02pmdb1g6w7ngnyiuwd17.node0'curl --location --request GET 'http://localhost:8181/rests/data/network-topology:network-topology/topology=topology-netconf/node=xr6_1/yang-ext:mount/Cisco-IOS-XR-ifmgr-cfg:interface-configurations/interface-configuration=act,Loopback97'\
--header 'Content-Type: application/json'\
--header 'Accept: application/json'\
--header 'Authorization: Basic YWRtaW46YWRtaW4='\
--header 'Cookie: JSESSIONID=node0cxljma02pmdb1g6w7ngnyiuwd17.node0'
Response for all three requests (description of interface is 'test loopback' - TX2 hasn't been committed):
{"errors":{"error":[{"error-message":"Uniconfig transaction 5160a334-aeb0-4864-931a-bc652990feb0 has already been closed","error-tag":"access-denied","error-type":"protocol"}]}}
RPC Response, Status: 403
{"errors":{"error":[{"error-message":"Uniconfig transaction 14804ad7-c0f6-4b22-8336-7484efc23e31 has already been closed","error-tag":"access-denied","error-type":"protocol"}]}}