Transferring between accounts – part 3

The correct implementation to the transfer problem will look like the following code (the full code sample is attached with the code bundle):

from pymongo import MongoClient
import json

class InitData:
def __init__(self):
self.client = MongoClient('localhost', 27017, w='majority')
self.db = self.client.mongo_bank
self.accounts = self.db.accounts

# drop data from accounts collection every time to start from a clean slate
self.accounts.drop()

init_data = InitData.load_data(self)
self.insert_data(init_data)
self.transfer('1', '2', 300)

@staticmethod
def load_data(self):
ret = []
with open('init_data.json', 'r') as f:
for line in f:
ret.append(json.loads(line))
return ret

def insert_data(self, data):
for document in data:
collection_name = document['collection']
account_id = document['account_id']
account_name = document['account_name']
account_balance = document['account_balance']

self.db[collection_name].insert_one({'account_id': account_id, 'name': account_name, 'balance': account_balance})

# validating errors, using the tx session
def tx_transfer_err_ses(self, source_account, target_account, value):
print(f'transferring {value} Hypnotons from {source_account} to {target_account}')
with self.client.start_session() as ses:
ses.start_transaction()
res = self.accounts.update_one({'account_id': source_account}, {'$inc': {'balance': value * (-1)}}, session=ses)
res2 = self.accounts.update_one({'account_id': target_account}, {'$inc': {'balance': value}}, session=ses)
error_tx = self.__validate_transfer_ses(source_account, target_account, ses)

if error_tx['status'] == True:
print(f"cant transfer {value} Hypnotons from {source_account} ({error_tx['s_bal']}) to {target_account} ({error_tx['t_bal']})")
ses.abort_transaction()
else:
ses.commit_transaction()

# we are passing the session value so that we can view the updated values
def __validate_transfer_ses(self, source_account, target_account, ses):
source_balance = self.accounts.find_one({'account_id': source_account}, session=ses)['balance']
target_balance = self.accounts.find_one({'account_id': target_account}, session=ses)['balance']
if source_balance < 0 or target_balance < 0:
return {'status': True, 's_bal': source_balance, 't_bal': target_balance}
else:
return {'status': False}

def main():
InitData()

if __name__ == '__main__':
main()

In this case, by passing the session object's ses value, we ensure that we can both make changes in our database using update_one() and also view these changes using find_one(), before doing either an abort_transaction() operation or a commit_transaction() operation.

Transactions cannot perform data definition language (DDL) operations, so drop(), create_collection(), and other operations that can affect MongoDB's DDL will fail inside a transaction. This is why we are setting w='majority' in our MongoClient object, to make sure that, when we drop a collection right before we start our transaction, this change will be visible to the transaction.

Even if we explicitly take care not to create or remove collections during a transaction, there are operations that will implicitly do so.
We need to make sure that the collection exists before we attempt to insert or upsert (update and insert) a document.

In the end, using transactions if we need to rollback, we don't need to keep track of the previous account balance values, as MongoDB will discard all of the changes that we made inside the transaction scope.

Continuing with the same example using Ruby, we have the following code for part 3:

require 'mongo'

class MongoBank
def initialize
@client = Mongo::Client.new([ '127.0.0.1:27017' ], database: :mongo_bank)
db = @client.database
@collection = db[:accounts]

# drop any existing data
@collection.drop

@collection.insert_one('collection': 'accounts', 'account_id': '1', 'account_name': 'Alex', 'account_balance':100)
@collection.insert_one('collection': 'accounts', 'account_id': '2', 'account_name': 'Mary', 'account_balance':50)

transfer('1', '2', 30)
transfer('1', '2', 300)
end

def transfer(source_account, target_account, value)
puts "transferring #{value} Hypnotons from #{source_account} to #{target_account}"
session = @client.start_session

session.start_transaction(read_concern: { level: :snapshot }, write_concern: { w: :majority })
@collection.update_one({ account_id: source_account }, { '$inc' => { account_balance: value*(-1)} }, session: session)
@collection.update_one({ account_id: target_account }, { '$inc' => { account_balance: value} }, session: session)

source_account_balance = @collection.find({ account_id: source_account }, session: session).first['account_balance']

if source_account_balance < 0
session.abort_transaction
else
session.commit_transaction
end
end

end

# initialize class
MongoBank.new

Alongside all of the points raised in the example in Python, we find that we can also customize read_concern and write_concern per transaction.

The available read_concern levels for multi-document ACID transactions are as follows:

  • majority: A majority of the servers in a replica set have acknowledged the data. For this to work as expected in transactions, they must also use write_concern to majority.
  • local: Only the local server has acknowledged the data.
  • snapshot: The default read_concern levels for transactions as of MongoDB 4.0. If the transaction commits with majority as write_concern, all transaction operations will have read from a snapshot of majority committed data, otherwise no guarantee can be made.
Read concern for transactions is set in the transaction level or higher (session or, finally, client). Setting read concern in individual operations is not supported and is generally discouraged.

The available write_concern levels for multi-document ACID transactions are the same as everywhere else in MongoDB, except for w:0 (no acknowledgement), which is not supported at all.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset