// check the locale
Locale locale = this.checkLocale(context);
// setup the engine and context
DispatchContext ctx = localContext.get(localName);
GenericEngine engine = this.getGenericEngine(modelService.engineName);
// set IN attributes with default-value as applicable
modelService.updateDefaultValues(context, ModelService.IN_PARAM);
Map<String, Object> ecaContext = null;
// for isolated transactions
Transaction parentTransaction = null;
// start the transaction
boolean beganTrans = false;
try {
//Debug.logInfo("=========================== " + modelService.name + " 1 tx status =" + TransactionUtil.getStatusString() + ", modelService.requireNewTransaction=" + modelService.requireNewTransaction + ", modelService.useTransaction=" + modelService.useTransaction + ", TransactionUtil.isTransactionInPlace()=" + TransactionUtil.isTransactionInPlace(), module);
if (modelService.useTransaction) {
if (TransactionUtil.isTransactionInPlace()) {
// if a new transaction is needed, do it here; if not do nothing, just use current tx
if (modelService.requireNewTransaction) {
parentTransaction = TransactionUtil.suspend();
if (TransactionUtil.isTransactionInPlace()) {
throw new GenericTransactionException("In service " + modelService.name + " transaction is still in place after suspend, status is " + TransactionUtil.getStatusString());
}
// now start a new transaction
beganTrans = TransactionUtil.begin(modelService.transactionTimeout);
}
} else {
beganTrans = TransactionUtil.begin(modelService.transactionTimeout);
}
// enlist for XAResource debugging
if (beganTrans && TransactionUtil.debugResources) {
DebugXaResource dxa = new DebugXaResource(modelService.name);
try {
dxa.enlist();
} catch (Exception e) {
Debug.logError(e, module);
}
}
}
try {
int lockRetriesRemaining = LOCK_RETRIES;
boolean needsLockRetry = false;
do {
lockRetriesRemaining--;
// NOTE: general pattern here is to do everything up to the main service call, and retry it all if
//needed because those will be part of the same transaction and have been rolled back
// TODO: if there is an ECA called async or in a new transaction it won't get rolled back
//but will be called again, which means the service may complete multiple times! that would be for
//pre-invoke and earlier events only of course
// setup global transaction ECA listeners to execute later
if (eventMap != null) ServiceEcaUtil.evalRules(modelService.name, eventMap, "global-rollback", ctx, context, result, isError, isFailure);
if (eventMap != null) ServiceEcaUtil.evalRules(modelService.name, eventMap, "global-commit", ctx, context, result, isError, isFailure);
// pre-auth ECA
if (eventMap != null) ServiceEcaUtil.evalRules(modelService.name, eventMap, "auth", ctx, context, result, isError, isFailure);
// check for pre-auth failure/errors
isFailure = ServiceUtil.isFailure(result);
isError = ServiceUtil.isError(result);
//Debug.logInfo("After [" + modelService.name + "] pre-auth ECA, before auth; isFailure=" + isFailure + ", isError=" + isError, module);
context = checkAuth(localName, context, modelService);
GenericValue userLogin = (GenericValue) context.get("userLogin");
if (modelService.auth && userLogin == null) {
throw new ServiceAuthException("User authorization is required for this service: " + modelService.name + modelService.debugInfo());
}
// now that we have authed, if there is a userLogin, set the EE userIdentifier
if (userLogin != null && userLogin.getString("userLoginId") != null) {
GenericDelegator.pushUserIdentifier(userLogin.getString("userLoginId"));
}
// pre-validate ECA
if (eventMap != null) ServiceEcaUtil.evalRules(modelService.name, eventMap, "in-validate", ctx, context, result, isError, isFailure);
// check for pre-validate failure/errors
isFailure = ServiceUtil.isFailure(result);
isError = ServiceUtil.isError(result);
//Debug.logInfo("After [" + modelService.name + "] pre-in-validate ECA, before in-validate; isFailure=" + isFailure + ", isError=" + isError, module);
// validate the context
if (modelService.validate && !isError && !isFailure) {
try {
modelService.validate(context, ModelService.IN_PARAM, locale);
} catch (ServiceValidationException e) {
Debug.logError(e, "Incoming context (in runSync : " + modelService.name + ") does not match expected requirements", module);
throw e;
}
}
// pre-invoke ECA
if (eventMap != null) ServiceEcaUtil.evalRules(modelService.name, eventMap, "invoke", ctx, context, result, isError, isFailure);
// check for pre-invoke failure/errors
isFailure = ServiceUtil.isFailure(result);
isError = ServiceUtil.isError(result);
//Debug.logInfo("After [" + modelService.name + "] pre-invoke ECA, before invoke; isFailure=" + isFailure + ", isError=" + isError, module);
// ===== invoke the service =====
if (!isError && !isFailure) {
Map<String, Object> invokeResult = null;
if (serviceDebugMode) {
invokeResult = modelService.invoker.runSync(localName, engine, context);
modelService.invoker.sendCallbacks(engine, context, invokeResult, null, GenericEngine.SYNC_MODE);
} else {
invokeResult = engine.runSync(localName, modelService, context);
engine.sendCallbacks(modelService, context, invokeResult, GenericEngine.SYNC_MODE);
}
if (invokeResult != null) {
result.putAll(invokeResult);
} else {
Debug.logWarning("Service (in runSync : " + modelService.name + ") returns null result", module);
}
}
// re-check the errors/failures
isFailure = ServiceUtil.isFailure(result);
isError = ServiceUtil.isError(result);
//Debug.logInfo("After [" + modelService.name + "] invoke; isFailure=" + isFailure + ", isError=" + isError, module);
if (beganTrans) {
// crazy stuff here: see if there was a deadlock or other such error and if so retry... which we can ONLY do if we own the transaction!
String errMsg = ServiceUtil.getErrorMessage(result);
// look for the string DEADLOCK in an upper-cased error message; tested on: Derby, MySQL
// - Derby 10.2.2.0 deadlock string: "A lock could not be obtained due to a deadlock"
// - MySQL ? deadlock string: "Deadlock found when trying to get lock; try restarting transaction"
// - Postgres ? deadlock string: TODO
// - Other ? deadlock string: TODO
// TODO need testing in other databases because they all return different error messages for this!
// NOTE DEJ20070908 are there other things we need to check? I don't think so because these will
//be Entity Engine errors that will be caught and come back in an error message... IFF the
//service is written to not ignore it of course!
if (errMsg != null && errMsg.toUpperCase().indexOf("DEADLOCK") >= 0) {
// it's a deadlock! retry...
String retryMsg = "RETRYING SERVICE [" + modelService.name + "]: Deadlock error found in message [" + errMsg + "]; retry [" + (LOCK_RETRIES - lockRetriesRemaining) + "] of [" + LOCK_RETRIES + "]";
// make sure the old transaction is rolled back, and then start a new one
// if there is an exception in these things, let the big overall thing handle it
TransactionUtil.rollback(beganTrans, retryMsg, null);
beganTrans = TransactionUtil.begin(modelService.transactionTimeout);
// enlist for XAResource debugging
if (beganTrans && TransactionUtil.debugResources) {
DebugXaResource dxa = new DebugXaResource(modelService.name);
try {
dxa.enlist();
} catch (Exception e) {
Debug.logError(e, module);
}
}
if (!beganTrans) {
// just log and let things roll through, will be considered an error and ECAs, etc will run according to that
Debug.logError("After rollback attempt for lock retry did not begin a new transaction!", module);
} else {
// deadlocks can be resolved by retring immediately as conflicting operations in the other thread will have cleared
needsLockRetry = true;
// reset state variables
result = FastMap.newInstance();
isFailure = false;
isError = false;
Debug.logWarning(retryMsg, module);
}
// look for lock wait timeout error, retry in a different way by running after the parent transaction finishes, ie attach to parent tx
// - Derby 10.2.2.0 lock wait timeout string: "A lock could not be obtained within the time requested"
// - MySQL ? lock wait timeout string: "Lock wait timeout exceeded; try restarting transaction"
if (errMsg != null && (errMsg.indexOf("A lock could not be obtained within the time requested") >= 0 ||
errMsg.indexOf("Lock wait timeout exceeded") >= 0)) {
// TODO: add to run after parent tx
}
}
}
} while (needsLockRetry && lockRetriesRemaining > 0);
// create a new context with the results to pass to ECA services; necessary because caller may reuse this context
ecaContext = FastMap.newInstance();
ecaContext.putAll(context);
// copy all results: don't worry parameters that aren't allowed won't be passed to the ECA services
ecaContext.putAll(result);
// setup default OUT values
modelService.updateDefaultValues(context, ModelService.OUT_PARAM);
// validate the result
if (modelService.validate && validateOut) {
// pre-out-validate ECA
if (eventMap != null) ServiceEcaUtil.evalRules(modelService.name, eventMap, "out-validate", ctx, ecaContext, result, isError, isFailure);
try {
modelService.validate(result, ModelService.OUT_PARAM, locale);
} catch (ServiceValidationException e) {
throw new GenericServiceException("Outgoing result (in runSync : " + modelService.name + ") does not match expected requirements", e);
}
}
// pre-commit ECA
if (eventMap != null) ServiceEcaUtil.evalRules(modelService.name, eventMap, "commit", ctx, ecaContext, result, isError, isFailure);
// check for pre-commit failure/errors
isFailure = ServiceUtil.isFailure(result);
isError = ServiceUtil.isError(result);
// global-commit-post-run ECA, like global-commit but gets the context after the service is run
if (eventMap != null) ServiceEcaUtil.evalRules(modelService.name, eventMap, "global-commit-post-run", ctx, ecaContext, result, isError, isFailure);
// check for failure and log on info level; this is used for debugging
if (isFailure) {
Debug.logWarning("Service Failure [" + modelService.name + "]: " + ServiceUtil.getErrorMessage(result), module);
}
} catch (Throwable t) {
if (Debug.timingOn()) {
UtilTimer.closeTimer(localName + " / " + modelService.name, "Sync service failed...", module);
}
String errMsg = "Service [" + modelService.name + "] threw an unexpected exception/error";
if (serviceDebugMode) {
modelService.invoker.sendCallbacks(engine, context, null, t, GenericEngine.SYNC_MODE);
} else {
engine.sendCallbacks(modelService, context, t, GenericEngine.SYNC_MODE);
}
try {
TransactionUtil.rollback(beganTrans, errMsg, t);
} catch (GenericTransactionException te) {
Debug.logError(te, "Cannot rollback transaction", module);