Why you must add try/catch blocks to Java-based BPM scripts

On very rare occasions, you will find that you need to create a BPM script using a Java-based vuser type instead of a C-based virtual user type. There is one nasty gotcha to keep in mind if you need to do this.

Here is a partial code snippet from a BPM script for an that emulates a program that connects to a database, and runs some queries.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public int init() throws Throwable {
 
  // Print user and environment details
  lr.output_message("$$ Server: " + lr.eval_string("<Server>"));
  lr.output_message("$$ Database: " + lr.eval_string("<Database>"));
  lr.output_message("$$ Account: " + lr.eval_string("<Username>"));
  lr.output_message("$$ Java Version: " + System.getProperty("java.version"));
 
  lr.start_transaction("login");
 
  // Connect to database
  _oracledatasource1 = new oracle.jdbc.pool.OracleDataSource();
  _oracledatasource1.setDriverType("thin");
  _oracledatasource1.setLoginTimeout(5);
  _oracledatasource1.setUser(lr.eval_string("<Username>"));
  _oracledatasource1.setPassword(lr.eval_string("<Password>"));
  _oracledatasource1.setServerName(lr.eval_string("<Servername>"));
  _oracledatasource1.setPortNumber(1521);
  _oracledatasource1.setDatabaseName(lr.eval_string("<Database>"));
  _string1 = _oracledatasource1.getURL();
  _connection1 = _oracledatasource1.getConnection();
 
  // Print account expiry date
  _oraclestatement1 = new oracle.jdbc.driver.OracleStatement((oracle.jdbc.driver.OracleConnection)_connection1, 1, 10);
  _resultset1 = ((oracle.jdbc.driver.OracleStatement)_oraclestatement1).executeQuery("SELECT EXPIRY_DATE, SYSDATE FROM USER_USERS");
  _resultset1.next();
  lr.output_message("$$ User account expires on: " + _resultset1.getString(1));
 
  lr.end_transaction("login", lr.AUTO);
 
  return 0;
}

At a glance, the code looks simple, correct and robust. It even prints some helpful troubleshooting information to the log.

Unfortunately, if the getConnection() method throws an exception, then BAC will not fail the login transaction (because it never gets to the end_transaction statement), instead it will show that the script is just not running. Of course this does not trigger an alert because the transaction is listed as “no run”, rather than “fail”. This is an extremely dangerous situation, as the BPM script will not alert anyone if the application becomes unavailable.

Here is a screenshot of the BAC Transaction Analysis page for a profile that had this problem…
BAC Transaction Analysis - No transactions scheduled

The solution to this problem is to wrap method calls that can throw exceptions in a try/catch block, and ensure that the transaction is ended with “fail” status.

To ensure that each transaction is ended with fail status if an exception is thrown during the transaction steps, it seems best to wrap all steps in a transaction within a single try/catch block, with separate try/catch blocks for each transaction.

Here is the modified code…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public int init() throws Throwable {
  try {
    // Print user and environment details
    lr.output_message("$$ Server: " + lr.eval_string("<Server>"));
    lr.output_message("$$ Database: " + lr.eval_string("<Database>"));
    lr.output_message("$$ Account: " + lr.eval_string("<Username>"));
    lr.output_message("$$ Java Version: " + System.getProperty("java.version"));
 
    lr.start_transaction("log in");
 
    // Oracle connection details
    _oracledatasource1 = new oracle.jdbc.pool.OracleDataSource();
    _oracledatasource1.setDriverType("thin");
    _oracledatasource1.setLoginTimeout(5);
    _oracledatasource1.setUser(lr.eval_string("<Username>"));
    _oracledatasource1.setPassword(lr.eval_string("<Password>"));
    _oracledatasource1.setServerName(lr.eval_string("<Servername>"));
    _oracledatasource1.setPortNumber(1521);
    _oracledatasource1.setDatabaseName(lr.eval_string("<Database>"));
    _string1 = _oracledatasource1.getURL();
    _connection1 = _oracledatasource1.getConnection();
 
    // Print account expiry date
    _oraclestatement1 = new oracle.jdbc.driver.OracleStatement((oracle.jdbc.driver.OracleConnection)_connection1, 1, 10);
    _resultset1 = ((oracle.jdbc.driver.OracleStatement)_oraclestatement1).executeQuery("SELECT EXPIRY_DATE, SYSDATE FROM USER_USERS");
    _resultset1.next();
    lr.output_message("$$ Account " + lr.eval_string("<Username>") + "expires on: " + _resultset1.getString(1));
 
    lr.end_transaction("log in", lr.AUTO);
 
  } catch (Exception e) {
    // Print exception description, fail transaction, close connections, and rethrow exception.
    lr.error_message("$$ Exception thrown: " + e.toString());
    lr.end_transaction("log in", lr.FAIL);
    if (_connection1 != null) {
	_connection1.close(); // Close Connection object if it was successfully opened.
    }
    throw e;
  }
 
  return 0;
}

Note that this advice only really applies for methods that are not VuGen specific e.g. if it was a web.url() or web.submit_data() method that failed, then the error will be detected, and the transaction will be marked as failed without adding try/catch blocks.

1 comment

Stuart Moncrieff
Stuart Moncrieff

Obviously if the method had a checked exception, then the compiler would force you to put it inside a try/catch block.

In this case, SQLException is not a checked exception.

Also, it is not really necessary to add try/catch blocks to a VuGen script that will be used with LoadRunner, because if a vuser throws an exception and exits, it will be obvious from seeing a stopped vuser in the LoadRunner Controller window.

Leave a Reply