2

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[].
Grimdari
  • 353
  • 1
  • 16
  • Documentation on `cfthread`, such as https://cfdocs.org/cfthread will help you understand about variables inside threads. However, the simplest solution is to not use threads unless you want to have long running blocks of code execute concurrently. There is nothing in your code that suggests you need to use them. – Dan Bracuk Nov 03 '21 at 17:20
  • You are not wrong. However, as stated above, the code I am trying to make functional is a way for me to understand threads. I have read the documentation from Lucee, from Adobe and cfdocs.org. Unfortunately, and as I stated above in #4, I'm still having issues, thus my reason for a question here on StackOverflow. – Grimdari Nov 03 '21 at 17:57
  • What happens if you change `VARIABLES.theServers = APPLICATION.emailCheck.checkSMTPServer('#domains#',25,'','');` to `THREAD.theServers = APPLICATION.emailCheck.checkSMTPServer('#domains#',25,'','');`? – Dan Bracuk Nov 04 '21 at 18:37
  • Making that changes the error to: No matching property [THESERVERS] found in [string]. Good suggestion though. I changed several pieces to the "THREAD" scope and got mixed results. I'm not giving up on that being the answer I just can't narrow it down to exactly what needs to change. – Grimdari Nov 04 '21 at 18:50

1 Answers1

5

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

SOS
  • 6,430
  • 2
  • 11
  • 29
  • 1
    For a demo of the corrections, see https://trycf.com/gist/512ef48ae975ee8ec84c45aa442b9430/lucee5?theme=monokai – SOS Nov 04 '21 at 21:26
  • 1
    Oh my stars and garters! Thank you! This is exactly what I was looking for; an answer that has examples and shows me where my code was broke. I'm honored that you spent the time to write such an outstanding explanation. A million times thank you! – Grimdari Nov 04 '21 at 21:54
  • 1
    You're welcome! I'm sure someone else will come along and suggest other improvements or sleeker ways to do things (which is good too!) but I figured understanding the issues with the original example would be more helpful than throwing totally new code at you (: – SOS Nov 04 '21 at 22:13