scache_iov

Description

array scache_iov(resource session, array operations)
array $SCacheConnection->iov(array operations)

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.

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

session
Session resource returned from scache_open, scache_reset or scache_connect
operations
Array of operations specified above. See example below.

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";
?>