Sidebar

How do I use Shared Cache variables and mutexes in QIE?

0 votes
427 views
asked Aug 29, 2022 by nathan-c-4426 (540 points)
I see that QIE now supports shared cache variables and mutexes?  

Why would I use sharedCache variables when we already have messageCache, channelCache, systemVariables and scriptVariables where I could store data?

4 Answers

0 votes

Answer: Speed, Flexibility.

Speed comes with risk though.  If your QIE instance shuts down for any reason (power outage, upgrades, scheduled maintenance etc.), variables stored in sharedCache are lost.  Is this a bad thing?  No, not if you know what you’re doing.

Flexibility means you can store ANY type of object in the Shared Cache, including custom Java/JSON objects.  This means that you don’t have to spend time converting everything to Strings in Maps and can just use your objects as they are once created.  This can be both efficient and enjoyable!

Ok, so what’s a good use case for using Shared Cache variables? 

There are several but let’s cover 2 examples.

EXAMPLE 1

Let’s say we’re processing a large set of patient visits (100,000,000), but that when we receive these patient visits they’re mostly grouped by patient already.  Let’s also say that as part of our processing of these visits we’re looking in a database to find the patient the visit was for so that we can add some additional information to our visit messages (name/dob).

By looking up patients and caching them, we can avoid hitting the database for all 100,000,000 messages and instead just hit the database when we encounter a patient we haven’t hit before, or that has timed out.

Let’s say our initial message looked like this:


{ 
   "pid": 1238, 
   "location": "Zergs Pediatrics", 
   "reason_for_visit": "pain in knee joint", 
   "diagnosis": "bacterial infection"
} 

With a mapping node like this:


var pQuery;
var pid = message.getNode('pid');

// Check to see if we have already cached the patient by its pid
var results = qie.getSharedCache(pid);
if (results === null) {
	pQuery = qie.getParameterizedQuery('select name, dob from patient where pid = :pid');
	pQuery.setString('pid', pid);
	results = pQuery.doQuery('maria', false);
	qie.setSharedCache(pid, results, 30);
}
if (results !== null && results.getRowCount() > 0) {
	message.setNode('name', results.getNode('name'));
	message.setNode('dob', results.getNode('dob'));
}

We can see then that it carefully checks to see if we’ve already looked up the patient information. If we already have fetched the patient, then we just use that information without needing to retrieve it again and again, if not then we cache it and use it and it’s ready for the next time we see the patient. We chose to have the values expire after 30 seconds because it’s likely we process all visits for a patient within 30 seconds, but if not then it would just re-retrieve that patient and keep going. Obviously, this is a trivial example where it would be nearly just as fast to retrieve the data from the database, however if the query was more complex, performing a calculation of some sort then the benefits of caching would really shine.It’s also important to note that we have used
the cached variables safely, so that if the QIE service shuts down unexpectedly and we must restart, no data is at risk if those cached values are cleared out. They are easily re-retrieved on reboot, and everything then continues normally.

 

answered Aug 29, 2022 by nathan-c-4426 (540 points)
0 votes

EXAMPLE 2

This example will show the value of cached variables across multiple channels when storing complex objects. We will even cross zone boundaries! They’re also thread safe!

Let’s say we have 3 channels.One that processes people, one that processes cars, one that processes fountain pens.

Rules:

  • Every time a Tesla is purchased as celebration the next 3 people processed with the same first name as the Tesla purchaser will receive free Mont Blanc Fountain Pens.
  • Every time a Pelican Pen is purchased the next 2 cars purchased wil receive a 10% discount.

Our seed data for our 3 channels will look like this:

CARS


qie.addInboundMessage('{"car-id":,1,"brand":"Mercedes","cost":1000.00,"buyer-first-name":"Glenn"}','');
qie.addInboundMessage('{"car-id":,2,"brand":"Audi","cost":69.00,"buyer-first-name":"Jerry"}','');
qie.addInboundMessage('{"car-id":,3,"brand":"Chevrolet","cost":138.27,"buyer-first-name":"Spencer"}','');
qie.addInboundMessage('{"car-id":,4,"brand":"Toyota","cost":300.00,"buyer-first-name":"Tonya"}','');
qie.addInboundMessage('{"car-id":,5,"brand":"BMW","cost":100.00,"buyer-first-name":"Nathan"}','');
qie.addInboundMessage('{"car-id":,6,"brand":"Tesla","cost":130.00,"buyer-first-name":"Stephen"}','');
qie.addInboundMessage('{"car-id":,7,"brand":"Ford","cost":79.00,"buyer-first-name":"Catyana"}','');
qie.addInboundMessage('{"car-id":,8,"brand":"Audi","cost":69.00,"buyer-first-name":"David"}','');
qie.addInboundMessage('{"car-id":,9,"brand":"Toyota","cost":300.00,"buyer-first-name":"Jason"}','');
qie.addInboundMessage('{"car-id":,10,"brand":"Ford","cost":600.00,"buyer-first-name":"Michael"}','');
qie.addInboundMessage('{"car-id":,11,"brand":"Chevrolet","cost":138.27,"buyer-first-name":"Aaron"}','');
qie.addInboundMessage('{"car-id":,12,"brand":"Toyota","cost":300.00,"buyer-first-name":"Michelle"}','');
qie.addInboundMessage('{"car-id":,13,"brand":"BMW","cost":59.99,"buyer-first-name":"Rudy"}','');
qie.addInboundMessage('{"car-id":,14,"brand":"BMW","cost":59.99,"buyer-first-name":"Aimee"}','');
qie.addInboundMessage('{"car-id":,15,"brand":"Tesla","cost":130.00,"buyer-first-name":"Samuel"}','');
qie.addInboundMessage('{"car-id":,16,"brand":"Mercedes","cost":1000.00,"buyer-first-name":"Dean"}','');
qie.addInboundMessage('{"car-id":,17,"brand":"Chevrolet","cost":138.27,"buyer-first-name":"James"}','');
qie.addInboundMessage('{"car-id":,18,"brand":"Mercedes","cost":1000.00,"buyer-first-name":"Derrick"}','');
qie.addInboundMessage('{"car-id":,19,"brand":"Tesla","cost":130.00,"buyer-first-name":"Robert"}','');
qie.addInboundMessage('{"car-id":,20,"brand":"Toyota","cost":300.00,"buyer-first-name":"Michael"}','');

PENS


qie.addInboundMessage('{"pen-id":,1,"brand":"Mont Blanc","cost":1000.00}','');
qie.addInboundMessage('{"pen-id":,2,"brand":"Parker","cost":69.00}','');
qie.addInboundMessage('{"pen-id":,3,"brand":"Sailor","cost":138.27}','');
qie.addInboundMessage('{"pen-id":,4,"brand":"Pelikan","cost":300.00}','');
qie.addInboundMessage('{"pen-id":,5,"brand":"Sheaffer","cost":100.00}','');
qie.addInboundMessage('{"pen-id":,6,"brand":"Cross","cost":130.00}','');
qie.addInboundMessage('{"pen-id":,7,"brand":"Faber Castell","cost":79.00}','');
qie.addInboundMessage('{"pen-id":,8,"brand":"Parker","cost":69.00}','');
qie.addInboundMessage('{"pen-id":,9,"brand":"Pelikan","cost":300.00}','');
qie.addInboundMessage('{"pen-id":,10,"brand":"Faber Castell","cost":600.00}','');
qie.addInboundMessage('{"pen-id":,11,"brand":"Sailor","cost":138.27}','');
qie.addInboundMessage('{"pen-id":,12,"brand":"Pelikan","cost":300.00}','');
qie.addInboundMessage('{"pen-id":,13,"brand":"Sheaffer","cost":59.99}','');
qie.addInboundMessage('{"pen-id":,14,"brand":"Sheaffer","cost":59.99}','');
qie.addInboundMessage('{"pen-id":,15,"brand":"Cross","cost":130.00}','');
qie.addInboundMessage('{"pen-id":,16,"brand":"Mont Blanc","cost":1000.00}','');
qie.addInboundMessage('{"pen-id":,17,"brand":"Sailor","cost":138.27}','');
qie.addInboundMessage('{"pen-id":,18,"brand":"Mont Blanc","cost":1000.00}','');
qie.addInboundMessage('{"pen-id":,19,"brand":"Cross","cost":130.00}','');
qie.addInboundMessage('{"pen-id":,20,"brand":"Pelikan","cost":300.00}','');

 

answered Aug 29, 2022 by mike-r-7535 (13,830 points)
edited Aug 29, 2022 by nathan-c-4426
0 votes

PERSONS


qie.addInboundMessage('{"person-id":,1,"first-name":"James","last-name":"Dean","pens":[]}','');
qie.addInboundMessage('{"person-id":,2,"first-name":"Nathan","last-name":"Davidson","pens":[]}','');
qie.addInboundMessage('{"person-id":,3,"first-name":"David","last-name":"Shapiro","pens":[]}','');
qie.addInboundMessage('{"person-id":,4,"first-name":"Michael","last-name":"Jones","pens":[]}','');
qie.addInboundMessage('{"person-id":,5,"first-name":"Aaron","last-name":"Adamson","pens":[]}','');
qie.addInboundMessage('{"person-id":,6,"first-name":"Christa","last-name":"Pigg","pens":[]}','');
qie.addInboundMessage('{"person-id":,7,"first-name":"Paul","last-name":"Perron","pens":[]}','');
qie.addInboundMessage('{"person-id":,8,"first-name":"Muhamad","last-name":"Smith","pens":[]}','');
qie.addInboundMessage('{"person-id":,9,"first-name":"Jin","last-name":"Xi","pens":[]}','');
qie.addInboundMessage('{"person-id":,10,"first-name":"Stewart","last-name":"Hodgson","pens":[]}','');
qie.addInboundMessage('{"person-id":,11,"first-name":"Glenn","last-name":"Ross","pens":[]}','');
qie.addInboundMessage('{"person-id":,12,"first-name":"Nathan","last-name":"Martinez","pens":[]}','');
qie.addInboundMessage('{"person-id":,13,"first-name":"James","last-name":"Sanchez","pens":[]}','');
qie.addInboundMessage('{"person-id":,14,"first-name":"Christa","last-name":"Collette","pens":[]}','');
qie.addInboundMessage('{"person-id":,15,"first-name":"Aaron","last-name":"Rodgers","pens":[]}','');
qie.addInboundMessage('{"person-id":,16,"first-name":"James","last-name":"Martinez","pens":[]}','');
qie.addInboundMessage('{"person-id":,17,"first-name":"Michael","last-name":"McCune","pens":[]}','');
qie.addInboundMessage('{"person-id":,18,"first-name":"Stephen","last-name":"Roberts","pens":[]}','');
qie.addInboundMessage('{"person-id":,19,"first-name":"Samuel","last-name":"El Lamanito","pens":[]}','');
qie.addInboundMessage('{"person-id":,20,"first-name":"James","last-name":"Coolio","pens":[]}','');

We know that across each of our channels, since they could be running in any order – that we need to make sure they initialize the object that keeps track of potential winners. To accomplish this we will have a published function that makes sure the object is correctly initialized.


function initializeDrawing() {
	try {
		qie.warn("I am initializing the drawing");
		// Even though cached variables are thread safe.You still need to code
		// so that they're logic safe ;)For that purpose we'll use mutexes.

		var mutexObtained = qie.getMutex('drawingStatus', 300000, 300, 'global', true);
		if (mutexObtained && null === qie.getSharedCache('drawingStatus', 'global', true)) {
			// create an empty JSON object to keep track of potential winners
			qie.setSharedCache('drawingStatus', {"cars":[],"pens":[]}, 9999999, 'global', true);
		}
	} finally {
		// Only the thread that obtained the mutex can release it, so there's no reason
		// to check to see if we successfully obtained the mutex since if we didn't we
		// won't be able to release it.
		qie.releaseMutex('drawingStatus', 'global', true);
	}
}

Now we’ll look at the Pens channel because it’s easiest.


var mutexObtained;
var i = 0;
var apply10PercentDiscount = false;

// Function to correctly initialize the object if it doesn't yet exist.
if (null == qie.getSharedCache('drawingStatus', 'global', true)) {
	initializeDrawing();
}

try {
	// We're processing pens.Update the status if someone is buying a Pelikan Pen.
	// We’ll use the mutex to make sure race conditions won't apply and then
	// only the correct # of winners will be chosen.
	// The mutex name should match the object being manipulated.
	if (message.getNode('brand') == 'Pelikan') {
		mutexObtained = qie.getMutex('drawingStatus', 30000, 300, 'global', true);
		if (mutexObtained) {
			qie.debug("A PELIKAN pen was purchased!Next car should get a discount");
			var drawingStatus = qie.getSharedCache('drawingStatus', 'global', true);
			drawingStatus.pens.push({"brand":"pelikan","count":0});
			// update the sharedCache object.
			qie.setSharedCache('drawingStatus', drawingStatus, 9999999, 'global', true);
		}
		qie.releaseMutex('drawingStatus', 'global', true);
		message.setNode("pelikanPurchased", "true");
	}
} finally {
	qie.releaseMutex('drawingStatus', 'global', true);
}

The logic is simple.If they’re buying a Pelikan pen, then we “push” a pen into the pens array of our cached object with a count of 0 that we will later increment up to the limit of winners, such that we could have a flexible # of winners if we wanted to change that in the future.

 

answered Aug 29, 2022 by nathan-c-2001 (140 points)
edited Aug 29, 2022 by nathan-c-4426
0 votes

The next channel that makes the most sense to look at is the Cars channel.Looking at it we find very similar logic.


var mutexObtained;
var i = 0;
var apply10PercentDiscount = false;
var drawingStatus;

if (null == qie.getSharedCache('drawingStatus', 'global', true)) {
	initializeDrawing();
}

try {
	// We're processing cars.Check to see if we qualify for a 10% discount cause
	// someone bought a Pelikan Pen.
	// We’ll use the mutex to make sure race conditions won't apply and then
	// only the correct # of winners is chosen.
	// The mutex name should match the object being manipulated.
	mutexObtained = qie.getMutex('drawingStatus', 300000, 300, 'global', true);
	if (mutexObtained) {
		drawingStatus = qie.getSharedCache('drawingStatus', 'global', true);
		for(i = 0;i < drawingStatus.pens.length; i++) {
			if (drawingStatus.pens[i].count < 2) {
				qie.debug("THEY WON! Will apply the 10% discount");
				// they won!
				apply10PercentDiscount = true;
				if (drawingStatus.pens[i].count++ == 2) {
					// erase it because we only discount the first 2 after pen purchase.
					drawingStatus.pens[i] = null;
				}
				// update the sharedCache object.
				qie.setSharedCache('drawingStatus', drawingStatus, 9999999, 'global', true);
				break;
			}
		}
	}
	qie.releaseMutex('drawingStatus', 'global', true);

	// Next, we see if they’re buying a tesla
	if (message.getNode('brand') == "Tesla") {
		qie.debug("They bought a tesla!");
		mutexObtained = qie.getMutex('drawingStatus', 300000, 300, 'global', true);
		if (mutexObtained) {
			drawingStatus = qie.getSharedCache('drawingStatus', 'global', true);
			drawingStatus.cars.push({"firstName":message.getNode('buyer-first-name'),"count":0});
			// push a new Tesla cars object into the cars object array
			qie.setSharedCache('drawingStatus', drawingStatus, 9999999, 'global', true);
		}
		qie.releaseMutex('drawingStatus', 'global', true);
	}
	// Process car, apply discount as necessary.
	if (apply10PercentDiscount) {
		message.setNode('cost', message.getNode('cost') - message.getNode('cost') * 0.1);
		message.setNode('discountApplied', 'true');
	}
} finally {
	/* There is no harm in “releasing” a mutex multiple times if it has already been released
	nothing additional will occur. It is always best practice to a release to mutexes in a finally
	block to account for exceptions that might occur unexpectedly. */
	qie.releaseMutex('drawingStatus', 'global', true);
}

Finally, we can look at the last channel People to see who is winning pens!


var mutexObtained;
var i = 0;
var freePen = false;

if (null == qie.getSharedCache('drawingStatus', 'global', true)) {
	initializeDrawing();
}

try {
	// We're processing people.Check to see if first name matches a tesla purchaser.
	// Again we'll use the mutex to make sure race conditions won't apply and to ensure
	// only the correct # of winners is chosen.
	// The mutex name should match the object being manipulated.
	mutexObtained = qie.getMutex('drawingStatus', 300000, 300, 'global', true);
	if (mutexObtained) {
		var drawingStatus = qie.getSharedCache('drawingStatus', 'global', true);
		for(i = 0;i < drawingStatus.cars.length; i++) {
			if (drawingStatus.cars[i].firstName == message.getNode("first-name") &&
				drawingStatus.cars[i].count < 3) {
				qie.debug("NAME matches tesla buyers name, they get a free pen!");
				freePen = true;
				if (drawingStatus.cars[i].winners++ == 3) {
					qie.debug("We are erasing because we got 3 people who won pens");
					// erase it because we only give 3 pens to people with the same name.
					drawingStatus.cars[i] = null;
				}
				// update the sharedCache object.
				qie.setSharedCache('drawingStatus', drawingStatus, 9999999, 'global', true);
				break;
			}
		}
	}
	qie.releaseMutex('drawingStatus', 'global', true);
	// Process the person.
	if (freePen) {
		message.addStringToJSONArray('pens', 'Pelikan');
		message.setNode('freePen', 'true');
	}
} finally {
	qie.releaseMutex('drawingStatus', 'global', true);
}

Shared Cache Summary:

  • Shared Caches are great for storing complex objects and the only place in QIE you can do that.
  • Shared Cache variables are cross channel, cross zone as needed.
  • They are thread safe--However, remember that if you’re storing data that could possibly change thread to thread make sure you protect the data with mutex boundaries during the period when you’re using/setting the data as needed.
  • Use timeouts that make sense given the data being stored.
  • Make sure you’re conscious of the potential memory requirements for storing the Shared Cache objects.
  • Shared Cache objects are forgotten upon server reboot, so code them to handle re-hydration.
  • They can be very efficient and speed up channels when used appropriately.

Mutex Summary:

  • Mutexes are like renting a room.  Only one party is allowed in the room while you’re renting.
  • Use them as a boundary to protect thread sensitive data like shared cache objects.
  • Don’t forget to release them once you’re done with them to avoid long timeouts that will kill performance.
answered Aug 29, 2022 by kacie-c-8759 (140 points)
edited Aug 29, 2022 by nathan-c-4426
...