Intermediate iRules: Nested Conditionals

Conditionals are a pretty standard tool in every programmer's toolbox. They are the functions that allow us to decided when we want certain actions to happen, based on, well, conditions that can be determined within our code. This concept is as old as compilers. Chances are, if you're writing code, you're going to be using a slew of these things, even in an Event based language like iRules.

iRules is no different than any other programming/scripting language when it comes to conditionals; we have them. Sure how they're implemented and what they look like change from language to language, but most of the same basic tools are there: if, else, switch, elseif, etc. Just about any example that you might run across on DevCentral is going to contain some example of these being put to use. Learning which conditional to use in each situation is an integral part to learning how to code effectively.

Once you have that under control, however, there's still plenty more to learn. Now that you're comfortable using a single conditional, what about starting to combine them? There are many times when it makes more sense to use a pair or more of conditionals in place of a single conditional along with logical operators. For example:

if { [HTTP::host] eq "bob.com" and [HTTP::uri] starts_with "/uri1" } {
  pool pool1
} elseif { [HTTP::host] eq "bob.com" and [HTTP::uri] starts_with "/uri2" } {
  pool pool2
} elseif { [HTTP::host] eq "bob.com" and [HTTP::uri] starts_with "/uri3" } {
  pool pool3
}

Can be re-written to use a pair of conditionals instead, making it far more efficient. To do this, you take the common case shared among the example strings and only perform that comparison once, and only perform the other comparisons if that result returns as desired. This is more easily described as nested conditionals, and it looks like this:

if { [HTTP::host] eq "bob.com" } {
  if {[HTTP::uri] starts_with "/uri1" } {
    pool pool1
  } elseif {[HTTP::uri] starts_with "/uri2" } {
    pool pool2
  } elseif {[HTTP::uri] starts_with "/uri3" } {
    pool pool3
  }
}

These two examples are logically equivalent, but the latter example is far more efficient. This is because in all the cases where the host is not equal to "bob.com", no other inspection needs to be done, whereas in the first example, you must perform the host check three times, as well as the uri check every single time, regardless of the fact that you could have stopped the process earlier.

While basic, this concept is important in general when coding. It becomes exponentially more important, as do almost all optimizations, when talking about programming in iRules. A script being executed on a server firing perhaps once per minute benefits from small optimizations. An iRule being executed somewhere in the order of 100,000 times per second benefits that much more. A slightly more interesting example, perhaps, is performing the same logical nesting while using different operators.

In this example we'll look at a series of if/elseif statements that are already using nesting, and take a look at how we might use the switch command to even further optimize things. I've seen multiple examples of people shying away from switch when nesting their logic because it looks odd to them or they're not quite sure how it should be structured. Hopefully this will help clear things up. First, the example using if statements:

when HTTP_REQUEST {
  if { [HTTP::host] eq "secure.domain.com" } { 
    HTTP::header insert "Client-IP:[IP::client_addr]"  
    pool sslServers 
  } elseif { [HTTP::host] eq "www.domain.com" } { 
    HTTP::header insert "Client-IP:[IP::client_addr]"    
    pool httpServers 
  } elseif { [HTTP::host] ends_with "domain.com" and [HTTP::uri] starts_with "/secure"} {
    HTTP::header insert "Client-IP:[IP::client_addr]"
    pool sslServers
  } elseif {[HTTP::host] ends_with "domain.com" and [HTTP::uri] starts_with "/login"} {
    HTTP::header insert "Client-IP:[IP::client_addr]"
    pool httpServers
  } elseif { [HTTP::host] eq "intranet.myhost.com" } { 
    HTTP::header insert "Client-IP:[IP::client_addr]"
    pool internal 
  } 
}

As you can see, this is completely functional and would do the job just fine. There are definitely some improvements that can be made, though. Let's try using a switch statement instead of several if comparisons for improved performance. To do that, we're going to have to use an if nested inside a switch comparison. While this might be new to some or look a bit odd if you're not used to it, it's completely valid and often times the most efficient you’re going to get. This is what the above code would look like cleaned up and put into a switch:

when HTTP_REQUEST {
  HTTP::header insert "Client-IP:[IP::client_addr]"
  switch -glob [HTTP::host] {
    "secure.domain.com" { pool sslServers }
    "www.domain.com" { pool httpServers }
    "*.domain.com" {
      if { [HTTP::uri] starts_with "/secure" } {
        pool sslServers
      } else {
        pool httpServers
      }
    }
    "intranet.myhost.com" { pool internal }
  }
}

As you can see this is not only easier to read and maintain, but it will also prove to be more efficient. We've moved to the more efficient switch structure, we've gotten rid of the repeat host comparisons that were happening above with the /secure vs /login uris, and while I was at it I got rid of all those examples of inserting a header, since that was happening in every case anyway.

Hopefully the benefit this technique can offer is clear, and these examples did the topic some justice. With any luck, you'll nest those conditionals with confidence now.

Updated Sep 10, 2024
Version 3.0
No CommentsBe the first to comment