Daryl's ColdFusion Notes

Notes from the field on ColdFusion (or related) technical issues.

Friday, January 25, 2013

ColdFusion 9 and SLF4J/Log4J Errors

I've been having some problems trying to incorporate ActiveMQ with CF9, mainly because of logger conflicts; CF tries to use and old version of Log4J with SLF4J, and this creates problems with later versions of SLF4J.  Some parts of CF appear to be bound directly to Log4J, however, so simply replacing Log4J is not an option.

Errors include:
  • "java.lang.ClassNotFoundException: org.slf4j.impl.StaticLoggerBinder" 
  • "SLF4J: This version of SLF4J requires log4j version 1.2.12 or later."
  • "java.lang.ExceptionInInitializerError at org.apache.activemq.ActiveMQConnection."

One of the really nice things about SLF4J is that it's designed to be replaced at *runtime*. Things compile against SLF4J, but then you (as systems administrator) can choose what implementation will do the actual logging, be it Java's own logger or something like Log4j. It's important to note that the original author of Log4J has abandoned that project and replaced it with a new logging implementation, called LOGBack.  So, we'll do exactly that: change SLF4J to log via LOGBack.


I wound up gutting and replacing SLF4J with a newer version that's backward-compatible, and using LOGBack as the actual logging implementation (instead of Log4J).

The directories involved will vary based on whether you're using a "standalone" or "multiserver" install.  There are a few directories involved, and I'll express them by name:

"ColdFusion lib directory":
  • standalone: coldfusion9/lib, or
  • mulitserver: jrun4/servers/[servername]/cfusion.ear/cfusion.war/WEB-INF/cfusion/lib
"Web app lib directory":
  • standalone: coldfusion9/wwwroot/web-inf/lib
  • mulitserver: jrun4/servers/[servername]/cfusion.ear/cfusion.war/WEB-INF/lib (notice "cfusion" disappeared)
"Web app classes directory":
  • standalone: coldfusion9/wwwroot/web-inf/classes
  • mulitserver: jrun4/servers/[servername]/cfusion.ear/cfusion.war/WEB-INF/classes
Here's the step by step:

1. Download SLF4J from http://www.slf4j.org/download.html and LOGBack from http://logback.qos.ch/download.html

2. Stop ColdFusion.  Otherwise, you won't be able to rename the jars in next step.

3. In your ColdFusion lib directory, rename* or delete the following files:
  • slf4j-api-1.5.6.jar --> slf4j-api-1.5.6.jar.old
  • slf4j-log4j12-1.5.6.jar --> slf4j-log4j12-1.5.6.jar.old
* do NOT rename them to .old.jar, or they will still be in the classpath!

This step has the effect of removing ColdFusion's current SLF4J implementation.

4. Place the new SLF4J implementation jar in the web app lib directory:
  • slf4j-api-1.7.2.jar
5. Place the LOGBack implementation jars in the  web app lib directory:
  • logback-core-1.0.9.jar
  • logback-classic-1.0.9.jar
6. Create a file named "logback.xml" in your web app classes directory, with the following contents:
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
(If you don't create a logback.xml to change your root logging level to level="info" the amount of logging will likely bring a busy server to its knees.)

7. Start ColdFusion

Now you can place any other jars you need (eg, ActiveMQ.jar) in the web app lib directory and you won't get SLF4J errors.

Thursday, June 23, 2011

SQL Server db_executor Role

OK, so SQL Server still doesn't have a db_executor role.

Here's a way to add one to any given database:

 
if not exists (select * from sys.database_principals where name='db_executor' and type='R')
 create role db_executor

declare @name sysname
declare @sql nvarchar(250)

declare cur cursor for
 select name
 from sys.procedures
 order by name

open cur
fetch next from cur into @name
while @@FETCH_STATUS = 0 begin
 set @sql = N'grant execute on OBJECT::'+@name+N' to db_executor'
 print @sql
 exec sp_executesql @sql
 fetch next from cur into @name
end

close cur
deallocate cur

You'll need to re-run this every time you create a new stored procedure. You could expand on this to loop through all non-Master databases, or to use the DDL trigger feature of SQL Server 2005+, like this:

 
create trigger add_executor_role
on database
for create_procedure
as begin
 [sql from above goes here]
end

(For more on DDL triggers, see Using DDL Triggers in SQL Server 2005 to Capture Schema Changes)

Tuesday, June 01, 2010

CreateODBCDate Retains Time Data

Had an interesting "WTF moment" over the weekend. I tried to prevent a batch job from running on Memorial Day by using the following code:


<cfif dateCompare(createOBDCDate(now()), "May 31, 2010") is 0>
<cfabort />
</cfif>


But this failed to stop the job from running..? So I investigated further:


<cfoutput>
<cfset today = createOBDCDate(now()) />
#today#<br />
#createODBCDate("May 31, 2010")#<br />
#datecompare(today, "May 31, 2010")#
</cfoutput>


Which produced:


{d '2010-05-31'}
{d '2010-05-31'}
1


Okay... so those dates are different. WTF?


<cfoutput>
<cfset today = createOBDCDate(now()) />
#today#<br />
#timeformat(today)#<br />
#createODBCDate("May 31, 2010")#<br />
#datecompare(today, createODBCDate("May 31, 2010"))#
</cfoutput>


This returned:


{d '2010-05-31'}
12:11 PM
{d '2010-05-31'}
1


So... while CreateODBCDate() returns a formatted SQL date-only string, it invisibly retains the time information. Wow.

I wound up doing it the hard way:


<cfset today = createDate(year(now()), month(now()), day(now()))>


<sigh />

Monday, January 26, 2009

ListFirst, ListRest in SQL Server

So, this has probably been done a million times before, but I needed ColdFusion-style ListFirst and ListRest functions for creating a stored procedure in a database that violates First Normal Form. <sigh/>

Note that the behavior varies slightly from CF. The delimiter is forced to be a comma, and multiple consecutive delimiters are treated as multiple elements. (CF eating 3 commas at a time always annoyed me.)

So, here's the DDL for them:


CREATE FUNCTION [dbo].[listFirst] (@list nvarchar(4000))
RETURNS nvarchar(4000)
AS BEGIN
DECLARE @pos int
DECLARE @ret nvarchar(4000)
SET @pos = charindex(',', @list)
IF @pos > 0
SET @ret = left(@list, @pos-1)
ELSE
set @ret = @list
RETURN @ret
END


CREATE FUNCTION [dbo].[listRest] (@list nvarchar(4000))
RETURNS nvarchar(4000)
AS BEGIN
DECLARE @pos int
DECLARE @ret nvarchar(4000)
SET @pos = charindex(',', @list)
IF @pos > 0
SET @ret = substring(@list, @pos+1, len(@list)-@pos)
ELSE
SET @ret = ''
RETURN @ret
END


As a side note, it just occurred to me that you can probably work around the multiple consecutive delimiter thing by adding whitespace, then trimming:


<cfloop list="#replace(myList, ",", ", ", "ALL")#" index="item">
<cfset item=trim(item) />
...
</cfloop>


Why didn't I think of that years ago?

Wednesday, January 21, 2009

Automatically Fix Case Sensitivity Issues

When you're converting from Windows, with a case-insensitive file system, to just about any other operating system, you'll likely run into file naming issues.

If you have a file named MyInclude.cfm, and you try to use it in ColdFusion as <cfinclude template="myInclude.cfm" /> (note the lowercase "m"), this will work fine in Windows, but then fail on the new OS.

I've written a small template to automagically repair most of these problems. and you can download it here: http://www.cfprimer.com/download.cfm?ffFile=_referenceFixer.cfm

To plagarize mercilessly from its comments:


Purpose:

Fix file and directory name references in files, for transitioning from Windows (with case
insensitive filenames) to another OS (eg Linux) with case sensitive file names.

NOTE: If you have two different files (perhaps in different directories) with the same name,
but different case, this will not work as well as hoped. (All references will get set to
one or the other case.)

Use:

Place _referenceFixer.cfm in a document directory and load.
Template will start from its current directory and proceed to find all file and directory
names in that directory and its subdirectories.

If "updateFiles" is set to True, the files will be rewritten and references fixed.

"fileExtensions" should be set to list the files whose references should be checked
and (if updateFiles is true) fixed, eg. ".cfm,.html".

Templates beginning with an underscore character ("_") will be skipped.

Do NOT leave this on production servers..!

Tuesday, January 20, 2009

ColdFusion 8 Professional with Windows IIS7 64-Bit

ColdFusion 8 will run on 64-bit Windows with a 64-bit JRE, but only if you pay for Enterprise.  While the ColdFusion 8 32-bit installer will run on Windows 64, the IIS connector fails miserably.  I worked around this problem by using the IIS Connector from the 64-bit installation with a ColdFusion instance running with its 32-bit installation.

Fist, install the "normal" ColdFusion 32-bit version, but select "Built-in web server (Development use only)" on the Configure Web Servers/Websites screen:



Next, install the 64-bit version of ColdFusion, with the following options:
  • On the "Install Type" screen, choose "Developer Edition":

  • On the "Subcomponent Installation" screen, install no complementary services.

  • On the Installation Directory screen, choose "ColdFusion64" (or any dir other than where you installed 32-bit ColdFusion)

  • On the Configure Web Servers/Websites screen, select "All IIS websites" (which is the default).
At this point, you may be able to hit http://127.0.0.1/CFIDE/administrator/index.cfm to configure your ColdFusion instance.  If not, check the following items:
  • Run regedt32, and verify that the ImagePath attribute in the ColdFusion 8 Application Server service (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ColdFusion 8 Application Server) is set to "C:\ColdFusion8\runtime\bin\jrunsvc.exe"
  • Ensure that the "ColdFusion 8 Application Server" service is started.  If there's a problem, open a command prompt, CD \Coldfusion8\bin, and run CFSTART.  Look for errors.
  • Open C:\ColdFusion8\runtime\servers\coldfusion\SERVER-INF\jrun.xml.  Search for "JRunProxyService"; make sure the "deactivated" attribute is "false" and that the "port" attribute is "51011".  (If you need to fix either of these, ColdFusion must be restarted.)
  • Make sure the 64-bit connector is also pointing to 51011: in the file C:\ColdFusion64\runtime\lib\wsconfig\1\jrun_iis6_wildcard.ini file, the "bootstrap" entry should be set to "bootstrap=127.0.0.1:51011".  If you need to fix this, IIS must be restarted.
Note that you'll never actually start the 64-bit instance, but it must be left in place because you're using its web server connector.

As a side note-- if you try to start ColdFusion 8 Professional in a 64-bit JVM (with either installation type), ColdFusion will revert to a "Developer" license.

Update: If the above doesn't work for you, see Jason Holden's Post on the same subject.

Friday, January 16, 2009

ColdFusion: Fast Query to File Export


A semi-common data processing task is to take the result of a query and write it to a file. People usually do this in ColdFusion something like this:


<cfsetting requesttimeout="1200" enablecfoutputonly="yes">
<cfset tab = chr(9)>

<cfquery datasource="Northwind" name="qProducts">
  SELECT p1.ProductID, p1.ProductName, case when p2.productname> 'B' then p2.productName else null end productName2
  FROM Products p1
  CROSS JOIN Products p2
  CROSS JOIN Products p3
  CROSS JOIN Products p4
  WHERE p1.ProductName <> 'foo'
  ORDER BY p1.ProductID
</cfquery>

<cfloop query="qProducts">
  <cfset line = ProductID
        & tab & ProductName
        & tab & ProductName2>
  <cffile action="append"
        file="/out.txt"
        output="#line#"
        addNewLine="yes"
    >
</cfloop>
<cfsetting enablecfoutputonly="no">


This works fine for small result sets. In my case, however, I Cartesian-joined the Northwind "Products" table to itself a few times, so that this query returns 35 million rows; the page above runs for ~3 minutes on my laptop and then dies with a java.lang.OutOfMemoryError.

I'd previously written a set of ColdFusion tags that could access JDBC directly from ColdFusion and process the resultset one row at a time, thereby avoiding out-of-memory errors:


<cfsetting requesttimeout="1200" enablecfoutputonly="yes">
<cfset tab = chr(9)>

<!--- cf_jdbcQuery will return a java ResultSet object instead of a ColdFusion Query object --->
<cf_jdbcquery datasource="Northwind" name="qProducts"><cfoutput>
  SELECT p1.ProductID, p1.ProductName, case when p2.productname> 'B' then p2.productName else null end productName2
  FROM Products p1
  CROSS JOIN Products p2
  CROSS JOIN Products p3
  CROSS JOIN Products p4
  WHERE p1.ProductName <> 'foo'
  ORDER BY p1.ProductID
</cfoutput></cf_jdbcquery>

<cf_jdbcqueryloop query="qProducts">
  <cfset line = qProducts.ProductID
        & tab & qProducts.ProductName
        & tab & qProducts.ProductName2>
  <cffile action="append"
        file="/out.txt"
        output="#line#"
        addNewLine="yes"
    >
</cf_jdbcqueryloop>
<cfsetting enablecfoutputonly="no">


This ran for 20mins (until it hit the timeout),writing about 37MB to the output file. Still... 20 minutes? I think we can do better.

So, I wrote a Java class to do this in a very small loop, to see the difference in speed. The Java source file ("FastResultsetToFile.java") is this:


import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;

/**
* @author Daryl Banttari
* Released to the public domain.  Do with it what you will!
*/
public class FastResultsetToFile {

  public static final byte[] CRLF = { 13, 10 };

  /**
   * Very quickly sends data from an any size query to an output file.
   *
   * @throws IOException
   * @throws SQLException
   */
  public static int exportResultsetToFile(ResultSet rs, String fileName, String delim, String charset) throws IOException, SQLException {
      int rowcount = 0;
      File file = new File(fileName);
      byte[] delimBytes = delim.getBytes(charset);
      byte[] CRLF = "\r\n".getBytes(charset);
      BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
      try {
          ResultSetMetaData rsmd = rs.getMetaData();
          int numCols = rsmd.getColumnCount();
          // iterate over resultset
          while (rs.next()) {
              // write each column
              for (int i = 1; i <= numCols; ++i) {
                  String s = rs.getString(i);
                  if (s != null) {
                      out.write(s.getBytes(charset));
                  }
                  if (i == numCols) {
                      // end of row, write CRLF
                      out.write(CRLF);
                  }
                  else {
                      // write delimiter between data
                      out.write(delimBytes);
                  }
              }
              rowcount++;
          }
      }
      finally {
          if (out != null) {
              out.flush();
              out.close();
          }
          if (rs != null) {
              rs.close();
          }
      }
      return rowcount;
  }

}


To compile this, save the FastResultsetToFile.java to disk, then type "javac FastResultsetToFile.java". This will produce a file called FastResultsetToFile.class. Depending on how you have ColdFusion installed, the file should be placed in either:

\coldfusionmx\wwwroot\WEB-INF\classes

or

\JRun4\servers\cfusion\cfusion-ear\cfusion-war\WEB-INF\classes

The ColdFusion page looks like this:


<!--- cf_jdbcQuery will return a java ResultSet object instead of a ColdFusion Query object --->
<cf_jdbcQuery datasource="Northwind" name="qProducts">
  SELECT p1.ProductID, p1.ProductName, case when p2.productname> 'B' then p2.productName else null end productName2
  FROM Products p1
  CROSS JOIN Products p2
  CROSS JOIN Products p3
  CROSS JOIN Products p4
  WHERE p1.ProductName <> 'foo'
  ORDER BY p1.ProductID
</cf_jdbcQuery>

<cfset application.fastFileWriter = createObject("java", "FastResultsetToFile")>
<cfset application.fastFileWriter.exportResultsetToFile(qProducts, "/out.txt", chr(9), "UTF-8")>

Done.


This completes, writing all 35,153,041 rows into a 1.3 GB file in about 5 minutes. Not bad! If we'd let cf_jdbcQueryLoop complete, it'd have taken nearly 12 hours (based on time and file size ratio).

While you can do a lot right inside ColdFusion, it's important to remember that it's not the right tool for every job.

Multiple ColdFusion Instances NEQ "Faster"

There's an oft-cited technote indicating that running with multiple instances somehow performs much better than running with a single instance, but no testing methodology is cited on the technote.

In my experience load testing, in the general case, the opposite is true-- you get better performance with fewer instances.  In the specific case where you're doing a very large number of very small requests, the single-threaded nature of the datasource connection pool becomes a bottleneck, but it's easier (and less resource intensive) to work around that specific issue by using a modest number of identically-configured datasources, and choosing a datasource randomly at the start of the request.

If you're actually memory constrained, then you're better off using a single large 64-bit instance vs. a bunch of small 32-bit instances.

(I proved via load testing to one customer that their app had 10-15% better throughput when three of their four instances were disabled, and the techie that had pushed for Enterprise + multiple instances said, and I quote, "I don't believe you."  Believe?  A result that changes consistently and predictably when one variable changes somehow involves "belief"?  What's happened to /science/ in this country?)