Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can’t I catch certain exceptions in a MarkLogic request?

I have some code that exercises the “invalid values” setting on an element range index. In this case, I have configured a dateTime element range index on the onDate element in my database (which will apply to both XML elements and JSON properties). I’ve set that index to reject invalid values. This setting means if I try to set the value of an onDate element and it is not castable to a dateTime or is null (literal null in JSON or xsi:nil="true" in XML), my update will fail. (The opposite behavior is to completely ignore invalid values.)

I tried the following code in Server-Side JavaScript in MarkLogic 8.0-4:

'use strict';
declareUpdate();
var errors = [];
var inputs = {
 '/37107-valid.json': (new Date()).toISOString(),
 '/37107-invalid.json': 'asdf',  // Should throw an error
 '/37107-null.json': null  
};

for(var uri in inputs) {
 try {
   xdmp.documentInsert(
     uri,
     { 'onDate': inputs[uri] },
     xdmp.defaultPermissions(),
     ['37107'] // Collections
   );
 } catch(err) {
   errors.push(err);
 }
}
errors.length;

I would have expected my request to succeed and to end up with 1 === errors.length, because only the second insert would have failed because 'asdf' is not castable as a dateTime and it is not null. However, instead I get an XDMP-RANGEINDEX error and my transaction fails. Why doesn’t my try/catch work here?

like image 918
Justin Makeig Avatar asked Mar 01 '16 19:03

Justin Makeig


1 Answers

The issue is how MarkLogic processes update transactions. Rather than actually changing the data with each xdmp.docuentInsert(…) call, MarkLogic queues up all of the updates and applies them atomically at the end of the request. (This is also why you can’t see database updates within the same transaction.) Thus, the error isn’t being thrown until after the loop has executed and the database tries to commit the queued transactions. This behavior is the same in XQuery (slightly simplified):

let $uris := (
 '/37107-valid.xml',
 '/37107-invalid.xml',
 '/37107-null.xml'
)
let $docs := (
 <onDate>{fn:current-dateTime()}</onDate>,
 <onDate>asdf</onDate>,
 <onDate xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
)
return
 for $uri at $i in $uris
 return 
   try {
     xdmp:document-insert($uri, $docs[$i], (), ('37107'))
   } catch($err) {
     xdmp:log($err)
   }

In order to catch the errors synchronously, you’d need to put each update into its own transaction. In general, this approach will be much slower and resource intensive than MarkLogic’s default transaction handling. However, it’s illustrative here to demonstrate what’s happening under the covers and can come in handy for specific use cases, like this one.

In the example below, I use xdmp.invokeFunction() to “call” a function in a separate transaction from the parent request. (First-class functions for the win!) This allows the updates to be fully applied (or rolled back with an error) and the calling module to see the updates (or errors). I’ve wrapped the low-level xdmp.invokeFunction() in my own applyAs() function to provide some niceties, like correctly passing function arguments to the curried function.

'use strict';

var errors = [];
var inputs = {
 '/37107-valid.json': (new Date()).toISOString(),
 '/37107-invalid.json': 'asdf',
 '/37107-null.json': null
};

var insert = applyAs(
 function(uri, value) {
   return xdmp.documentInsert(
     uri,
     { 'onDate': inputs[uri] },
     xdmp.defaultPermissions(),
     ['37107']
   );
 },
 { isolation: 'different-transaction', transactionMode: 'update' },
 'one'
);

for(var uri in inputs) {
 try {
   insert(uri, inputs[uri]);
 } catch(err) {
   errors.push(err);
 }
}
errors.length; // Correctly returns 1


// <https://gist.github.com/jmakeig/0a331823ad9a458167f6>
function applyAs(fct, options, returnType /* 'many', 'one', 'iterable' (default) */) {
  options = options || {};
  return function() {
    var params = Array.prototype.slice.call(arguments); 
    // Curry the function to include the params by closure.
    // xdmp.invokeFunction requires that invoked functions have
    // an arity of zero.
    var f = (function() {
       return fct.apply(null, params);
    }).bind(this);
    // Allow passing in user name, rather than id
    if(options.user) { options.userId = xdmp.user(options.user); delete options.user; }
    // Allow the functions themselves to declare their transaction mode
    if(fct.transactionMode && !(options.transactionMode)) { options.transactionMode = fct.transactionMode; }
    var result = xdmp.invokeFunction(f, options); // xdmp.invokeFunction returns a ValueIterator
    switch(returnType) {
      case 'one':
        // return fn.head(result); // 8.0-5
        return result.next().value;
      case 'many':
        return result.toArray();
      case 'iterable':
      default:
        return result;
    }
  }
}
like image 193
Justin Makeig Avatar answered Oct 08 '22 12:10

Justin Makeig