coldfusionluceecfloopcfthread

Lucee/Adobe ColdFusion - Setting/getting information from thread{}


I'm working on creating a simple email server status page that calls two different CFCs.

The status page requirements:

  1. Query a MariaDB database table via a CFC and return data from two fields: server_name (ie. MyServerName) & server_domain (ie. mail.domain.com). Currently, there are 4 rows in the database table to pull.
  2. Hand the database data from step 1 to a CFC that checks if port 25 is listening. If the CFC can reach port 25 the result is true, if not the result is false. This step needs to be threaded.
  3. Hand the boolean result from step 2 through a loop to print the server_name and boolean result.

Output something similar to this:
MyServerName - <up arrow>
MyServerName2 - <up arrow>
MyServerName3 - <up arrow>
MyServerName4 - <down arrow>

The code:

    RetrieveEmailServers = APPLICATION.selectQueries.RetrieveEmailServers()
    if (RetrieveEmailServers.recordCount) {
        for(i = 1; i <= RetrieveEmailServers.recordCount(); i++) {
            LOCAL.theDomains = RetrieveEmailServers.check_servers_domain[i];
            LOCAL.theNames = RetrieveEmailServers.check_servers_name[i];
            thread action="run" name="thread#i#" theDomains="#LOCAL.theDomains#" theNames="#LOCAL.theNames#" {
                VARIABLES.theServers = APPLICATION.emailCheck.checkSMTPServer('#domains#',25,'','');
            }
        }
        thread action="join" timeout="6000"{}

        for(i = 1; i <= RetrieveEmailServers.recordCount(); i++) {
            VARIABLES.theResult = cfthread["thread#i#"];
            if (VARIABLES.theResult.theServers) {
                LOCAL.theStatus = "<i class='fad fa-angle-double-up text-success fs-1'></i>"
            }
            else {
                LOCAL.theStatus = "<i class='fad fa-angle-double-down text-danger fs-1'></i>"
            } 
            writeOutput(ATTRIBUTES.theNames & " - " & LOCAL.theStatus & "<br>");
        }
    }
    else {
        writeOutput("No servers listed at this time.")
    }

The error: The key [THESERVERS] does not exist, the structure is empty

For consideration:

  1. I know my code is not great and I know it could be written better. I'm working hard to improve.
  2. I'm not a full time coder but I have been coding off and on for many years. I still consider myself a newbie with CFML so a lot of the methodology goes over my head.
  3. The above code mostly works but I'm having difficulty understanding how to pass information outside of a CFTHREAD to be used in the rest of the page, especially when dealing with CFLOOP.
  4. I have read many times, but still don't completely understand, how to correctly use the thread-local scope, the Thread scope and the Attributes scope.
  5. The code above has a simple task, to check a port, but the end goal is to use similar code for other parts of my application. I'm aware there are better monitoring tools available; this is an exercise to help me understand and learn.
  6. Specific to Lucee, I'm aware that threadData()['thread#i#'].status; or similar may be a required modification to cfthread[].

Solution

  • Attributes

    The Attributes scope is only for holding values passed into a thread. Therefore, the scope is short-lived and only exists within a thread. Each thread has its own "attributes" scope, which doesn't exist before that thread runs or after it completes.

    For example, this snippet passes in an attribute named "theDomains". The variable Attributes.theDomains only exists inside the thread.

     thread action="run" name="thread1" theDomains="example.com" {
         writeDump( attributes.theDomains );
     }
     
     thread action="join" name="thread1" {};
     
     writeOutput( thread1.output );
    

    Thread-local

    "Thread-local" is another short-lived scope whose purpose is to hold variables used only within a thread. Each thread has its own private "local" scope, separate from all other threads. Like the attributes scope, it only exists while a thread is executing and is wiped out when the thread completes.

    For example, this snippet creates a local variable named "MyLocalVar". Displaying the thread output demonstrates the variable exists within the thread

    thread action="run" name="thread1" {
        // Un-scoped v
        myLocalVar = "foo";
        writeOutput( "myLocalVar ="& myLocalVar );
    }
    
    thread action="join" name="thread1" {};
    writeOutput( thread1.output );
    

    But attempting to access it after the thread completes will cause an error

    // fails with error "key [MYLOCALVAR] doesn't exist"
    writeOutput( "myLocalVar ="& thread1.myLocalVar );
    

    Thread Scope

    The Thread scope has a longer life-span. It's designed to store "..thread-specific variables and metadata about the thread...". More importantly, this scope can be used to pass information back to the calling page (or even other threads).

    For example, this snippet creates a thread scoped variable that's visible to the calling page, even after the thread completes its execution:

     thread action="run" name="thread1" {
        // use scope prefix "thread."
        thread.myThreadVar = "foo";
     }
     
     thread action="join" name="thread1" {};
     
     writeOutput( "thread1.myThreadVar="& thread1.myThreadVar );
     writeDump( thread1 );
    

    The Problem: key [THESERVERS] does not exist

    When you've been looking at an error for what feels like days, it's easy to forget the basics :) First thing to do with undefined errors is dump the object and see if actually contains what you expected before trying to use it.

    for(i = 1; i <= RetrieveEmailServers.recordCount(); i++) {
        VARIABLES.theResult = cfthread["thread#i#"];
        writeDump( variables.theResult );
    
        /* temporarily comment out rest of code 
        ...
        */
    }
    

    A dump of VARIABLES.theResult shows the threads are actually failing with a different error

    CFDump of Thread 1

    Due to the wrong attribute name within the thread. It should be attributes.theDomains, not domains.

    thread ...{
        APPLICATION.emailCheck.checkSMTPServer( attributes.theDomains, ... );
    }
    

    Okay, I fixed it. Still getting "key [THESERVERS] does not exist", now what?

    Another dump of the threads reveals the error message wasn't lying. The threads really DON'T contain a variable named theServers, due to incorrect scoping. Use the thread scope, not `variables.

     thread ...{
         thread.theServers = ....;
     }
    

    CFDump of Thread 2

    Yet another error?! variable [ATTRIBUTES] doesn't exist

    Even after fixing the previous two issues, you'll still get yet another error here

    for(i = 1; i <= RetrieveEmailServers.recordCount(); i++) {
       ...
       writeOutput(ATTRIBUTES.theNames & " - " & LOCAL.theStatus & "<br>");
    }
    

    Remember the attributes scope only exists within a thread. So obviously it can't be used once the thread is completed. Either store theNames in the thread scope too, or since you're looping through a query, use the query column value, RetrieveEmailServers.the_query_column_name[ i ].

    Joining threads

    One last potential problem. The join statement isn't actually waiting for the threads you created. It's just waiting for 6000 ms. If for some reason any of the threads take longer than that, you'll get an error when trying to retrieve the thread results. To actually wait for the threads created, you must use thread action="join" name=(list of thread names) {}. I'll leave that as an exercise for the reader (:

    Tbh, there are other things that could be cleaned up and/or improved, but hopefully this long rambling thread explains WHY the errors occurred in the first place AND how to avoid them when working with threads in future