Wednesday, November 18, 2015

Querying Using Non-Persistent Fields

I recently ran into a situation where I wanted to make a non-persistent field searchable in a table filter. The requirement was to make a table searchable based on whether a complex condition was true or false.

First, I created a non-persistent field on the table through Database Configuration.

Next, I added the field to my table and made it filterable. This was done by adding filterable="true" to the tablecol definition.

<tablecol dataattribute="CID_DEFECT" id="cid_defect" filterable="true" inputmode="readonly"/>


Normally, non-persistent fields are not filterable. I created the following Java class to make non-persistent fields work in a table filter.  The package space is important because it requires replacing the value of a package protected field.

package psdi.mbo;

import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import psdi.security.UserInfo;
import psdi.util.MXException;

/**
 * An MboQuery object that will accept some non-persistent fields without
 * adding them to the where clause.  This is to permit a non-persistent field
 * in a table filter row where the sql logic is implemented in the databean.
 *
 * @author Martin Nichol
 *
 */
public class NPMboQbe extends MboQbe {

    /** the list of permitted non-persistent attributes and their values.  */
    private HashMap<String, String> permittedAttributes = new HashMap<String, String>();

    /**
     * Constructor.
     *
     * @param ms mbosetinfo of the owning mboset.
     * @param processML the processML flag of the owning mboset.
     * @param userInfo the user who owns the owning mboset
     * @param npAttributes the non-persistent attributes to permit.
     */
    public NPMboQbe(MboSetInfo ms, boolean processML, UserInfo userInfo, String[] npAttributes) {
        super(ms, processML, userInfo);
        initializeNpFields(npAttributes);
    }

    /**
     * @param ms mbosetinfo of the owning mboset.
     * @param l the user's locale.
     * @param tz the user's timezone.
     * @param ml the processML flag of the owning mboset.
     * @param userInfo the user who owns the owning mboset.
     * @param npAttributes the non-persistent attributes to permit.
     */
    public NPMboQbe(MboSetInfo ms, Locale l, TimeZone tz, boolean ml, UserInfo userInfo, String[] npAttributes) {
        super(ms, l, tz, ml, userInfo);
        initializeNpFields(npAttributes);
    }


    @Override
    public void setQbe(String attr, String expr) throws MXException {
        if (permittedAttributes.containsKey(attr)) {
            permittedAttributes.put(attr, expr);
        } else {
            super.setQbe(attr, expr);
        }
    }

    @Override
    public String getQbe(String attrStr) throws MXException {
        if (permittedAttributes.containsKey(attrStr)) {
            return permittedAttributes.get(attrStr);
        } else {
            return super.getQbe(attrStr);
        }
    }

    @Override
    public void resetQbe() {
        super.resetQbe();
        for (Map.Entry me : permittedAttributes.entrySet()) {
            me.setValue("");
        }
    }

    /**
     * From a list of attributes, initialize the map of values.
     * @param npAttributes the list of attributes to allow.
     */
    private void initializeNpFields(String[] npAttributes) {
        for (String attr : npAttributes) {
            permittedAttributes.put(attr, "");
        }
    }

    /**
     * Associate an NPMboQbe with a given MboSet.
     * @param msr the mboset to replace the MboQbe object.
     * @param npAttributes the list of attributes to allow.
     * @throws RemoteException if an RMI problem occurs.
     * @throws MXException if a Maximo problem occurs.
     */
    public static void assignNewQbe(MboSetRemote msr, String[] npAttributes) throws RemoteException, MXException {
        MboSet ms = (MboSet)msr;
        MboQbe qbe = new NPMboQbe(ms.getMboSetInfo(), ms.getClientLocale(), ms.getClientTimeZone(), ms.processML(), ms.getUserInfo(), npAttributes);
        ms.qbe = qbe;
    }
}


Then I extended the DataBean associated with the table to apply the sql driven by the non-persistent field. If a DataBean isn't defined for the table, create it and assign it.

@Override
public synchronized void setQbe(String attribute, String expression) throws MXException {
    super.setQbe(attribute, expression);
    if ("CID_DEFECT".equals(attribute)) {
       if (expression.contains("Y")) {
          getMboSet().setWhere("sql expression for condition true");
       } else if (expression.contains("N")) {
          getMboSet().setWhere("sql expression for condition false");
       } else {
          getMboSet().setWhere("");
       }
    }
}

@Override
protected void initialize() throws MXException, RemoteException {
    super.initialize();
    NPMboQbe.assignNewQbe(getMboSet(), new String[] { "CID_DEFECT" });
}

The non-persistent field can now affect the results displayed in the table.

Crons in Maximo

When a Cron is Scheduled

When the CRONTASKINSTANCE is changed, it is made active or the schedule is modified, the new values are saved to the database and the RELOADREQTIME field is updated with the current time.

In each Maximo server, a background thread, called the Cron Monitor Thread runs.  Every 60 seconds (or how many seconds are defined in mxe.cronTaskMonitorInterval property) it looks at CRONTASKINSTANCE for all records with a RELOADREQTIME greater than the last time it checked.  If the cron isn't running and the instance is marked ACTIVE=1, then a new cron task thread is started for the cron. If the cron is running and the instance is marked ACTIVE=0, then the cron thread is woken and told to shutdown. If the cron is running and the instance is marked ACTIVE=1, then the thread is woken and told that an update is pending.  If Maximo is in ADMIN MODE, the cron monitor thread doesn't do anything.

When a new cron thread is started, it goes through these steps:

  1. A new Java Thread is started
  2. sleep 60 seconds, or how many seconds are defined in mxe.cronTaskInitDelay
  3. call the cron's init() method
  4. insert a new record in TASKSCHEDULER table if needed
  5. get last run information from TASKSCHEDULER table.
  6. calculate the next run time
  7. call the cron's start() method.
  8. loop and sleep until the next scheduled date arrives
  9. when the next scheduled date arrives
    if the last cron run was less than 300 minutes ago, only the maximo server that executed the cron last time can run the cron now, update TASKSCHEDULER with latest run information.
    if the last cron run was more than 300 minutes ago, this server can claim the cron and run it.  Update TASKSCHEDULER with the latest run information
  10. call the cron's action() method.


Cron Shutdown follows these steps:

  1. Cron task is marked for shutdown
  2. Thread is woken up.
  3. call the cron's stop() method.
  4. call the cron's shutdown() method.

These steps occur within each maximo server that can run crons.

Why do I have to schedule tasks to run in the future if I want to run them now?

When the Reload Request action is run, it updates the RELOADREQTIME on the CRONTASKINSTANCE record.  When the Cron Monitor Thread run's, it sets a pendingUpdate flag and attempts to wake up the thread.  Depending on where it's sleeping when it is awoken, it can slip into the action() branch before it realizes that the schedule has changed.  If the cron is already running when the pending flag is set, it might see the flag and act on it or it might not depending on when the flag is set.

The 60 second monitor interval plus the 60 second pause before intialization makes an absolute lower bound of at least two minutes before the next run time is calculated.  The longer the cron's init method takes to execute, that will push out the amount of time in advance the cron must be scheduled.  The number of cron nodes will also impact the amount of time in advance to schedule the crons.  Cron threads are started on each cron node in the cluster.  There can be database contention on TASKSCHEDULER that will add an additional amount of time before the next run date is calculated in step 6.

Finally, there is also clock skew to consider.  The person entering the scheduled time is either looking at their computer time, watch or cell phone.  Maximo schedules based on the time on the database server.  If the database machine is running faster than the user's clock, then after all the delays mentioned earlier, the scheduled time will have passed by the time it comes to determine the next run time.