scache_iov
Description
scache_iov is the swiss army knife and most efficient way to operate on scache session. It allows multiple commands to be issued in single request thus avoiding network latencies specific to single separate function operations. scache_iov operations are also atomic in a way that no other client is served until all iops are performed. However iop-operations are no transactional, some of they might succeed while some won't.
Supported operations are below. Parameters are expressed in same way and order like their single function equivalents. Return values are also equal.
With smart usage of accumulator, scache_iov is practically the most important and the most useful function.
- SCIOP_GET equals scache_get
- SCIOP_SET equals scache_set
- SCIOP_ADD equals scache_add
- SCIOP_REPLACE equals scache_replace
- SCIOP_UNSET and SCIOP_CLEAR equals scache_get
- SCIOP_STAT equals scache_stat
- SCIOP_COLL equals scache_coll
- SCIOP_SHGET equals scache_shget
- SCIOP_SHSET equals scache_shset
- SCIOP_SHADD equals scache_shadd
- SCIOP_SHREPLACE equals scache_shreplace
- SCIOP_SHUNSET and SCIOP_SHCLEAR equals scache_shget
- SCIOP_SHSTAT equals scache_shstat
- SCIOP_SHCOLL equals scache_shcoll
- SCIOP_CHGET equals scache_chget
- SCIOP_CHPUT equals scache_chput
- SCIOP_CHCLEAR equals scache_chclear
- SCIOP_VGET equals scache_vget
- SCIOP_VSET equals scache_vset
- SCIOP_VADD equals scache_vadd
- SCIOP_VSUB equals scache_vsub
- SCIOP_VUNSET and SCIOP_VCLEAR equals scache_vunset
- SCIOP_RNPUSH equals scache_rnpush
- SCIOP_RNUNSHIFT equals scache_rnunshift
- SCIOP_RNPOP equals scache_rnpop
- SCIOP_RNSHIFT equals scache_rnshift
- SCIOP_RNGET equals scache_rnget
- SCIOP_RNROTF equals scache_rnrotf
- SCIOP_RNROTB equals scache_rnrotb
- SCIOP_RNSIZE equals scache_rnsize
- SCIOP_RNCLEAR equals scache_rnclear
- SCIOP_VBRK has not function equivalent. VBRK is available only in scache_iov -call for conditionally terminating execution from currently running IOP-array.
Note that there exist no SCIOP_EXIST and SCIOP_SHEXIST. Use SCIOP_STAT and SCIOP_SHSTAT instead. Single call functions scache_exists and scache_shexists are actually just local wrappers over scache_*stat-functions and not recognized as individual operations by backend itself.
Parameters
Return values
Array of returned values or FALSE on failure. In case of failure, value returned by scache_lasterr is unspecified.
Errors during execution
From release 0.99.3 onward scache_iov supports IOP-operand SCIOP_LASTERR.
SCIOP_LASTERR retrieves and stores status code of immediately preceding IOP operation.
<?php $conn = scache_preset('LasterrTest'); $iops = Array(Array(SCIOP_SET, '/exists', 'yes'), Array(SCIOP_LASTERR), Array(SCIOP_GET, '/notexists'), Array(SCIOP_LASTERR), Array(SCIOP_ADD, '/exists', 'add-never-overwrites'), Array(SCIOP_LASTERR)); list($setres, $seterr, $getres, $geterr, $addres, $adderr) = $res = scache_iov($conn, $iops); /* results ($res) Array ( [0] => 1 [1] => 0 [2] => [3] => 3 [4] => [5] => 2 ) scache_strerror($seterr) == 'SCERR_SUCCESS' scache_strerror($geterr) == 'SCERR_NOTEXIST' scache_strerror($adderr) == 'SCERR_EXIST' */
Breaking with SCIOP_VBRK
From release 0.99.2 onward scache_iov supports IOP-operand SCIOP_VBRK.
Basically scripts fetch source data, operate on it and return some result. In busy environments where multiple clients change that source data, before pushing calculated result to be jointly used, there is sometimes a need to determine if source data is still valid or has it already been invalidated by another client.
Simplified example for described race condition :
<?php function is_state_changed($state) { /* test... something and return true or false */ } function calculate_new_shared_state($changes) { /* do heavy calculations and eventually return result to be sent to client... */ return $state; } $changeset = $_POST['changeset']; $state = scache_chget($session, 'cached_state'); if (!$state || is_state_changed($changeset)) { $state = calculate_new_shared_state($changeset); /* NOTE! Race condition if task interrupted here */ scache_chput($session, 'cached_state', $state); } return $state; ?>
In example above, if two threads happens to process state changes simultaneously, there is risk in multitasking environment that execution gets interrupted by kernel in marked position between new state calculation and pushing it to the cache and CPU is given to another thread to complete and cache its state change request.
When first thread gets resumed to execution to continue from where it was interrupted, it pushes it's resulted $state to cache, but the data from it was calculated, has already been updated by second thread in meantime. Because there is no way to detect this, first thread overwrites second thread's more resent cached $state with it's old result.
This is for what SCIOP_VBRK is for. VBRK gets path to the shared counter and it's expected value as its arguments. If counter's value does not match when SCIOP_VBRK is executed, all remaining IOPs are skipped with SCERR_BREAK.
Actually currently VBRK does not skip all operations. Only data modifying IOPs are skipped and data retrieving operations still generate their results.
This is still to be decided whether this is the right semantics or should it skip all operations. Actual result to stored data is same in both cases, but while skipping all is faster, it could be useful to be able to retrieve data even after writes would not be accomplished.
The race condition in above example could of course be solved by locking script to the one excecutor at time, but this is available only for single server installations. If there is multiple webservers involved, proper locking is generally not available but possible simultaneous changes must be detected some other way.
Above example written with SCIOP_VBRK would be :
<?php function is_state_changed($state) { /* test... something and return true or false */ } function calculate_new_shared_state($changes) { /* do heavy calculations and eventually return result to be sent to client... */ return $state; } $changeset = $_POST['changeset']; list($state, $current_counter) = /* get also current "serial" */ scache_iov($session, Array(Array(SCIOP_CHGET, 'cached_state'), Array(SCIOP_VGET, 'change_trigger'))); if (!$state || is_state_changed($changeset)) { $state = calculate_new_shared_state($changeset); /* BREAK if 'change_trigger' has be changed from what we queried it before and increment counter list($was_breaked, $was_stored, $counter) = scache_iov($session, Array(Array(SCIOP_VBRK, 'change_trigger', $current_counter), Array(SCIOP_CHPUT, 'cached_state', $state), Array(SCIOP_VADD, 'change_trigger', 1))); /* SCIOP_VBRK returns true, if break happened. So in this case we discard all cached results because not being sure of it's current state */ if ($was_breaked) scache_chclear($session, 'cached_state'); } return $state; ?>
Please note that there is gotcha in incrementing the counter. In example above, because of being skipped, "change_trigger" does not get incremented if SCIOP_VBRK breaks. If you need to ensure it gets incremented always, it needs to be written as :
list($counter, $was_breaked, $was_stored) = scache_iov($session, Array(Array(SCIOP_VADD, 'change_trigger', 1), Array(SCIOP_VBRK, 'change_trigger', ($current_counter + 1)), Array(SCIOP_CHPUT, 'cached_state', $state)));
Accumulator
From release 0.99.1 onward scache_iov supports usage of accumulator.
Accumulator stores last successfully queried or set value to be optionally utilized by next IOP on queue. In other words, current IOP can utilize data of it's immediately preceeding IOP.
Accumulator is referenced by omitting value (3rd parameter) from data storing operations. In example below, first IOP is normal three-parameter set operation that sets value to path specified. Second set-operation instead is given without it's normal third parameter so that value for it is picked from accumulator.
<?php $conn = scache_preset('AccumTest'); $res = scache_iov($conn, Array(Array(SCIOP_SET, 'my/path', 'this goes also to accum'), Array(SCIOP_SET, 'my/another/path'), Array(SCIOP_GET, 'my/another/path'))); /* results Array ( [0] => 1 [1] => 1 [2] => this goes also to accum ) */ ?>
For data storing operations, accumulator gets assigned the value to be stored. For data retrieving operations, last successfully retrieved value. If IOP operation fails, for example for SCERR_NOTEXIST, current accumulator value is not overwritten.
Accumulator is only available on scache_iov call. Accumulator does not span over multiple scache_iov -calls, but is always cleared on beginning of every call. Also, as not making sense, accumulator isn't available for counter (SCIOP_V*) operations.
It's possible to transfer values between different datastores via accumulator. For scache_iov being atomic operation, this might solve some concurrency issues that would otherwise require locking.
<?php $conn = scache_preset('AccumTest'); $res = scache_iov($conn, Array(Array(SCIOP_RNPOP, 'workqueue'), Array(SCIOP_SET, 'myworkunit'), Array(SCIOP_GET, 'myworkunit'))); ?>
In general, accumulator is feature without demonstrable obvious usage scenario, but it might be very helpful in some situations.
Notes
Most single operation functions are actually wrappers over scache_iov in module's internal implementation.
Examples
On login and session initialization we initialize certain values :
<?php $conn = scache_reset('MySess'); scache_set($conn, 'userinfo/loginname', 'guest'); scache_set($conn, 'userinfo/firstname', 'Visitor'); scache_set($conn, 'userinfo/surname', 'Account'); scache_set($conn, 'totalrequests', 0); ?>
On subsequent pages we collect necessary data in one request :
<?php list($loginname, $firstname, $surname, $totalrequests) = scache_iov($conn, Array(Array(SCIOP_GET, 'userinfo/loginname'), Array(SCIOP_GET, 'userinfo/firstname'), Array(SCIOP_GET, 'userinfo/surname'), Array(SCIOP_VADD, 'totalrequests', 1))); echo "Login : $loginname\n"; echo "Firstname : $firstname\n"; echo "Surname : $surname\n"; echo "Reqcount : $totalrequests\n"; ?>