Merging the queue-penalty branch. In short, this allows one to dynamically adjust
authorMark Michelson <mmichelson@digium.com>
Fri, 21 Dec 2007 00:44:17 +0000 (00:44 +0000)
committerMark Michelson <mmichelson@digium.com>
Fri, 21 Dec 2007 00:44:17 +0000 (00:44 +0000)
the QUEUE_MAX_PENALTY and the newly introduced QUEUE_MIN_PENALTY during a call depending
on the amount of time passed. The purpose is to allow the call to open up to more (or maybe
just different) members without the caller's losing his place in the queue. See
configs/queuerules.conf.sample for an example of how to set up queue rules and configs/queues.conf.sample
for how to associate a rule with a queue.

Along with the functional changes, new CLI and manager commands exist to show the rules defined and
there is an additional CLI command to reload the queue rules.

Future enhancements that may be made: support for realtime queue rules and support for dynamically adding
a rule through the manager or CLI. Also a manager command to reload the queue rules (I'll probably write
this myself very soon).

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@94370 65c4cc65-6c06-0410-ace0-fbb531ad65f3

apps/app_queue.c
configs/queuerules.conf.sample [new file with mode: 0644]
configs/queues.conf.sample
doc/tex/channelvariables.tex

index 7cf9a7c..c6dbfa8 100644 (file)
@@ -135,7 +135,7 @@ static char *app = "Queue";
 static char *synopsis = "Queue a call for a call queue";
 
 static char *descrip =
-"  Queue(queuename[,options[,URL][,announceoverride][,timeout][,AGI][,macro][,gosub]):\n"
+"  Queue(queuename[,options[,URL][,announceoverride][,timeout][,AGI][,macro][,gosub][,rule]):\n"
 "Queues an incoming call in a particular call queue as defined in queues.conf.\n"
 "This application will return to the dialplan if the queue does not exist, or\n"
 "any of the join options cause the caller to not enter the queue.\n"
@@ -170,6 +170,8 @@ static char *descrip =
 "calling party's channel once they are connected to a queue member.\n"
 "  The optional gosub parameter will run a gosub on the \n"
 "calling party's channel once they are connected to a queue member.\n"
+"  The optional rule parameter will cause the queue's defaultrule to be\n"
+"overridden by the rule specified.\n"
 "  The timeout will cause the queue to fail out after a specified number of\n"
 "seconds, checked between each queues.conf 'timeout' and 'retry' cycle.\n"
 "  This application sets the following channel variable upon completion:\n"
@@ -314,28 +316,31 @@ struct callattempt {
 
 
 struct queue_ent {
-       struct call_queue *parent;          /*!< What queue is our parent */
-       char moh[80];                       /*!< Name of musiconhold to be used */
-       char announce[80];                  /*!< Announcement to play for member when call is answered */
-       char context[AST_MAX_CONTEXT];      /*!< Context when user exits queue */
-       char digits[AST_MAX_EXTENSION];     /*!< Digits entered while in queue */
-       int valid_digits;                   /*!< Digits entered correspond to valid extension. Exited */
-       int pos;                            /*!< Where we are in the queue */
-       int prio;                           /*!< Our priority */
-       int last_pos_said;                  /*!< Last position we told the user */
-       time_t last_periodic_announce_time; /*!< The last time we played a periodic announcement */
-       int last_periodic_announce_sound;   /*!< The last periodic announcement we made */
-       time_t last_pos;                    /*!< Last time we told the user their position */
-       int opos;                           /*!< Where we started in the queue */
-       int handled;                        /*!< Whether our call was handled */
-       int pending;                        /*!< Non-zero if we are attempting to call a member */
-       int max_penalty;                    /*!< Limit the members that can take this call to this penalty or lower */
-       int linpos;                                                     /*!< If using linear strategy, what position are we at? */
-       int linwrapped;                                         /*!< Is the linpos wrapped? */
-       time_t start;                       /*!< When we started holding */
-       time_t expire;                      /*!< When this entry should expire (time out of queue) */
-       struct ast_channel *chan;           /*!< Our channel */
-       struct queue_ent *next;             /*!< The next queue entry */
+       struct call_queue *parent;             /*!< What queue is our parent */
+       char moh[80];                          /*!< Name of musiconhold to be used */
+       char announce[80];                     /*!< Announcement to play for member when call is answered */
+       char context[AST_MAX_CONTEXT];         /*!< Context when user exits queue */
+       char digits[AST_MAX_EXTENSION];        /*!< Digits entered while in queue */
+       int valid_digits;                              /*!< Digits entered correspond to valid extension. Exited */
+       int pos;                               /*!< Where we are in the queue */
+       int prio;                              /*!< Our priority */
+       int last_pos_said;                     /*!< Last position we told the user */
+       time_t last_periodic_announce_time;    /*!< The last time we played a periodic announcement */
+       int last_periodic_announce_sound;      /*!< The last periodic announcement we made */
+       time_t last_pos;                       /*!< Last time we told the user their position */
+       int opos;                              /*!< Where we started in the queue */
+       int handled;                           /*!< Whether our call was handled */
+       int pending;                           /*!< Non-zero if we are attempting to call a member */
+       int max_penalty;                       /*!< Limit the members that can take this call to this penalty or lower */
+       int min_penalty;                       /*!< Limit the members that can take this call to this penalty or higher */
+       int linpos;                                                        /*!< If using linear strategy, what position are we at? */
+       int linwrapped;                                            /*!< Is the linpos wrapped? */
+       time_t start;                          /*!< When we started holding */
+       time_t expire;                         /*!< When this entry should expire (time out of queue) */
+       struct ast_channel *chan;              /*!< Our channel */
+       AST_LIST_HEAD_NOLOCK(,penalty_rule) qe_rules; /*!< Local copy of the queue's penalty rules */
+       struct penalty_rule *pr;               /*!< Pointer to the next penalty rule to implement */
+       struct queue_ent *next;                /*!< The next queue entry */
 };
 
 struct member {
@@ -368,6 +373,15 @@ static AST_LIST_HEAD_STATIC(interfaces, member_interface);
 #define ANNOUNCEHOLDTIME_ONCE 2
 #define QUEUE_EVENT_VARIABLES 3
 
+struct penalty_rule {
+       int time;                           /*!< Number of seconds that need to pass before applying this rule */
+       int max_value;                      /*!< The amount specified in the penalty rule for max penalty */
+       int min_value;                      /*!< The amount specified in the penalty rule for min penalty */
+       int max_relative;                   /*!< Is the max adjustment relative? 1 for relative, 0 for absolute */
+       int min_relative;                   /*!< Is the min adjustment relative? 1 for relative, 0 for absolute */
+       AST_LIST_ENTRY(penalty_rule) list;  /*!< Next penalty_rule */
+};
+
 struct call_queue {
        AST_DECLARE_STRING_FIELDS(
                /*! Queue name */
@@ -382,6 +396,8 @@ struct call_queue {
                AST_STRING_FIELD(membermacro);
                /*! Gosub to run upon member connection */
                AST_STRING_FIELD(membergosub);
+               /*! Default rule to use if none specified in call to Queue() */
+               AST_STRING_FIELD(defaultrule);
                /*! Sound file: "Your call is now first in line" (def. queue-youarenext) */
                AST_STRING_FIELD(sound_next);
                /*! Sound file: "There are currently" (def. queue-thereare) */
@@ -456,8 +472,17 @@ struct call_queue {
        int membercount;
        struct queue_ent *head;             /*!< Head of the list of callers */
        AST_LIST_ENTRY(call_queue) list;    /*!< Next call queue */
+       AST_LIST_HEAD_NOLOCK(, penalty_rule) rules; /*!< The list of penalty rules to invoke */
 };
 
+struct rule_list {
+       char name[80];
+       AST_LIST_HEAD_NOLOCK(,penalty_rule) rules;
+       AST_LIST_ENTRY(rule_list) list;
+};
+
+AST_LIST_HEAD_STATIC(rule_lists, rule_list);
+
 static struct ao2_container *queues;
 
 static void update_realtime_members(struct call_queue *q);
@@ -570,7 +595,7 @@ enum queue_member_status {
        QUEUE_NORMAL
 };
 
-static enum queue_member_status get_member_status(struct call_queue *q, int max_penalty)
+static enum queue_member_status get_member_status(struct call_queue *q, int max_penalty, int min_penalty)
 {
        struct member *member;
        struct ao2_iterator mem_iter;
@@ -579,7 +604,7 @@ static enum queue_member_status get_member_status(struct call_queue *q, int max_
        ao2_lock(q);
        mem_iter = ao2_iterator_init(q->members, 0);
        for (; (member = ao2_iterator_next(&mem_iter)); ao2_ref(member, -1)) {
-               if (max_penalty && (member->penalty > max_penalty))
+               if ((max_penalty && (member->penalty > max_penalty)) || (min_penalty && (member->penalty < min_penalty)))
                        continue;
 
                switch (member->status) {
@@ -790,7 +815,7 @@ static struct member *create_queue_member(const char *interface, const char *mem
                cur->penalty = penalty;
                cur->paused = paused;
                ast_copy_string(cur->interface, interface, sizeof(cur->interface));
-               if(!ast_strlen_zero(membername))
+               if (!ast_strlen_zero(membername))
                        ast_copy_string(cur->membername, membername, sizeof(cur->membername));
                else
                        ast_copy_string(cur->membername, interface, sizeof(cur->membername));
@@ -834,6 +859,7 @@ static int member_cmp_fn(void *obj1, void *obj2, int flags)
 static void init_queue(struct call_queue *q)
 {
        int i;
+       struct penalty_rule *pr_iter;
 
        q->dead = 0;
        q->retry = DEFAULT_RETRY;
@@ -853,8 +879,8 @@ static void init_queue(struct call_queue *q)
        q->montype = montype_default;
        q->monfmt[0] = '\0';
        q->periodicannouncefrequency = 0;
-       if(!q->members) {
-               if(q->strategy == QUEUE_STRATEGY_LINEAR)
+       if (!q->members) {
+               if (q->strategy == QUEUE_STRATEGY_LINEAR)
                        /* linear strategy depends on order, so we have to place all members in a single bucket */
                        q->members = ao2_container_alloc(1, member_hash_fn, member_cmp_fn);
                else
@@ -880,6 +906,9 @@ static void init_queue(struct call_queue *q)
                if (q->sound_periodicannounce[i])
                        ast_str_set(&q->sound_periodicannounce[i], 0, "%s", "");
        }
+
+       while ((pr_iter = AST_LIST_REMOVE_HEAD(&q->rules,list)))
+               ast_free(pr_iter);
 }
 
 static void clear_queue(struct call_queue *q)
@@ -974,6 +1003,78 @@ static void clear_and_free_interfaces(void)
        AST_LIST_UNLOCK(&interfaces);
 }
 
+/*Note: call this with the rule_lists locked */
+static int insert_penaltychange (const char *list_name, const char *content, const int linenum)
+{
+       char *timestr, *maxstr, *minstr, *contentdup;
+       struct penalty_rule *rule = NULL, *rule_iter;
+       struct rule_list *rl_iter;
+       int time, inserted = 0;
+
+       if (!(rule = ast_calloc(1, sizeof(*rule)))) {
+               ast_log(LOG_ERROR, "Cannot allocate memory for penaltychange rule at line %d!\n", linenum);
+               return -1;
+       }
+
+       contentdup = ast_strdupa(content);
+       
+       if (!(maxstr = strchr(contentdup, ','))) {
+               ast_log(LOG_WARNING, "Improperly formatted penaltychange rule at line %d. Ignoring.\n", linenum);
+               ast_free(rule);
+               return -1;
+       }
+
+       *maxstr++ = '\0';
+       timestr = contentdup;
+
+       if ((time = atoi(timestr)) < 0) {
+               ast_log(LOG_WARNING, "Improper time parameter specified for penaltychange rule at line %d. Ignoring.\n", linenum);
+               ast_free(rule);
+               return -1;
+       }
+
+       rule->time = time;
+
+       if ((minstr = strchr(maxstr,',')))
+               *minstr++ = '\0';
+       
+       /* The last check will evaluate true if either no penalty change is indicated for a given rule
+        * OR if a min penalty change is indicated but no max penalty change is */
+       if (*maxstr == '+' || *maxstr == '-' || *maxstr == '\0') {
+               rule->max_relative = 1;
+       }
+
+       rule->max_value = atoi(maxstr);
+
+       if (!ast_strlen_zero(minstr)) {
+               if (*minstr == '+' || *minstr == '-')
+                       rule->min_relative = 1;
+               rule->min_value = atoi(minstr);
+       } else /*there was no minimum specified, so assume this means no change*/
+               rule->min_relative = 1;
+
+       /*We have the rule made, now we need to insert it where it belongs*/
+       AST_LIST_TRAVERSE(&rule_lists, rl_iter, list){
+               if (strcasecmp(rl_iter->name, list_name))
+                       continue;
+
+               AST_LIST_TRAVERSE_SAFE_BEGIN(&rl_iter->rules, rule_iter, list) {
+                       if (rule->time < rule_iter->time) {
+                               AST_LIST_INSERT_BEFORE_CURRENT(rule, list);
+                               inserted = 1;
+                               break;
+                       }
+               }
+               AST_LIST_TRAVERSE_SAFE_END;
+       
+               if (!inserted) {
+                       AST_LIST_INSERT_TAIL(&rl_iter->rules, rule, list);
+               }
+       }
+
+       return 0;
+}
+
 /*! \brief Configure a queue parameter.
 \par
    For error reporting, line number is passed for .conf static configuration.
@@ -1135,6 +1236,8 @@ static void queue_set_param(struct call_queue *q, const char *param, const char
                   we will not see any effect on use_weight until next reload. */
        } else if (!strcasecmp(param, "timeoutrestart")) {
                q->timeoutrestart = ast_true(val);
+       } else if (!strcasecmp(param, "defaultrule")) {
+               ast_string_field_set(q, defaultrule, val);
        } else if (failunknown) {
                if (linenum >= 0) {
                        ast_log(LOG_WARNING, "Unknown keyword in queue '%s': %s at line %d of queues.conf\n",
@@ -1396,14 +1499,14 @@ static int update_realtime_member_field(struct member *mem, const char *queue_na
        struct ast_variable *var;
        int ret = -1;
 
-       if(!(var = ast_load_realtime("queue_members", "interface", mem->interface, "queue_name", queue_name, NULL))) 
+       if (!(var = ast_load_realtime("queue_members", "interface", mem->interface, "queue_name", queue_name, NULL))) 
                return ret;
        while (var) {
-               if(!strcmp(var->name, "uniqueid"))
+               if (!strcmp(var->name, "uniqueid"))
                        break;
                var = var->next;
        }
-       if(var && !ast_strlen_zero(var->value)) {
+       if (var && !ast_strlen_zero(var->value)) {
                if ((ast_update_realtime("queue_members", "uniqueid", var->value, field, value, NULL)) > -1)
                        ret = 0;
        }
@@ -1472,7 +1575,7 @@ static int join_queue(char *queuename, struct queue_ent *qe, enum queue_result *
        ao2_lock(q);
 
        /* This is our one */
-       stat = get_member_status(q, qe->max_penalty);
+       stat = get_member_status(q, qe->max_penalty, qe->min_penalty);
        if (!q->joinempty && (stat == QUEUE_NO_MEMBERS))
                *reason = QUEUE_JOINEMPTY;
        else if ((q->joinempty == QUEUE_EMPTY_STRICT) && (stat == QUEUE_NO_REACHABLE_MEMBERS || stat == QUEUE_NO_UNPAUSED_REACHABLE_MEMBERS))
@@ -1705,6 +1808,7 @@ static void leave_queue(struct queue_ent *qe)
 {
        struct call_queue *q;
        struct queue_ent *cur, *prev = NULL;
+       struct penalty_rule *pr_iter;
        int pos = 0;
 
        if (!(q = qe->parent))
@@ -1727,6 +1831,9 @@ static void leave_queue(struct queue_ent *qe)
                                prev->next = cur->next;
                        else
                                q->head = cur->next;
+                       /* Free penalty rules */
+                       while ((pr_iter = AST_LIST_REMOVE_HEAD(&qe->qe_rules, list)))
+                               ast_free(pr_iter);
                } else {
                        /* Renumber the people after us in the queue based on a new count */
                        cur->pos = ++pos;
@@ -1736,8 +1843,8 @@ static void leave_queue(struct queue_ent *qe)
        ao2_unlock(q);
 
        /*If the queue is a realtime queue, check to see if it's still defined in real time*/
-       if(q->realtime) {
-               if(!ast_load_realtime("queues", "name", q->name, NULL))
+       if (q->realtime) {
+               if (!ast_load_realtime("queues", "name", q->name, NULL))
                        q->dead = 1;
        }
 
@@ -2219,7 +2326,7 @@ static struct callattempt *wait_for_answer(struct queue_ent *qe, struct callatte
        starttime = (long) time(NULL);
 #ifdef HAVE_EPOLL
        for (epollo = outgoing; epollo; epollo = epollo->q_next) {
-               if(epollo->chan)
+               if (epollo->chan)
                        ast_poll_channel_add(in, epollo->chan);
        }
 #endif
@@ -2424,8 +2531,8 @@ static struct callattempt *wait_for_answer(struct queue_ent *qe, struct callatte
        }
 
 #ifdef HAVE_EPOLL
-       for(epollo = outgoing; epollo; epollo = epollo->q_next) {
-               if(epollo->chan)
+       for (epollo = outgoing; epollo; epollo = epollo->q_next) {
+               if (epollo->chan)
                        ast_poll_channel_del(in, epollo->chan);
        }
 #endif
@@ -2498,6 +2605,28 @@ static int is_our_turn(struct queue_ent *qe)
        return res;
 }
 
+static void update_qe_rule(struct queue_ent *qe)
+{
+       int max_penalty = qe->pr->max_relative ? qe->max_penalty + qe->pr->max_value : qe->pr->max_value;
+       int min_penalty = qe->pr->min_relative ? qe->min_penalty + qe->pr->min_value : qe->pr->min_value;
+       char max_penalty_str[20], min_penalty_str[20]; 
+       /* a relative change to the penalty could put it below 0 */
+       if (max_penalty < 0)
+               max_penalty = 0;
+       if (min_penalty < 0)
+               min_penalty = 0;
+       if (min_penalty > max_penalty)
+               min_penalty = max_penalty;
+       snprintf(max_penalty_str, sizeof(max_penalty_str) - 1, "%d", max_penalty);
+       snprintf(min_penalty_str, sizeof(min_penalty_str) - 1, "%d", min_penalty);
+       pbx_builtin_setvar_helper(qe->chan, "QUEUE_MAX_PENALTY", max_penalty_str);
+       pbx_builtin_setvar_helper(qe->chan, "QUEUE_MIN_PENALTY", min_penalty_str);
+       qe->max_penalty = max_penalty;
+       qe->min_penalty = min_penalty;
+       ast_debug(3, "Setting max penalty to %d and min penalty to %d for caller %s since %d seconds have elapsed\n", qe->max_penalty, qe->min_penalty, qe->chan->name, qe->pr->time);
+       qe->pr = AST_LIST_NEXT(qe->pr, list);
+}
+
 static int wait_our_turn(struct queue_ent *qe, int ringing, enum queue_result *reason)
 {
        int res = 0;
@@ -2515,7 +2644,7 @@ static int wait_our_turn(struct queue_ent *qe, int ringing, enum queue_result *r
                        break;
                }
 
-               stat = get_member_status(qe->parent, qe->max_penalty);
+               stat = get_member_status(qe->parent, qe->max_penalty, qe->min_penalty);
 
                /* leave the queue if no agents, if enabled */
                if (qe->parent->leavewhenempty && (stat == QUEUE_NO_MEMBERS)) {
@@ -2548,6 +2677,11 @@ static int wait_our_turn(struct queue_ent *qe, int ringing, enum queue_result *r
                if (qe->parent->periodicannouncefrequency &&
                        (res = say_periodic_announcement(qe,ringing)))
                        break;
+               
+               /* see if we need to move to the next penalty level for this queue */
+               while (qe->pr && ((time(NULL) - qe->start) > qe->pr->time)) {
+                       update_qe_rule(qe);
+               }
 
                /* Wait a second before checking again */
                if ((res = ast_waitfordigit(qe->chan, RECHECK * 1000))) {
@@ -2597,7 +2731,7 @@ static int update_queue(struct call_queue *q, struct member *member, int callcom
 
 static int calc_metric(struct call_queue *q, struct member *mem, int pos, struct queue_ent *qe, struct callattempt *tmp)
 {
-       if (qe->max_penalty && (mem->penalty > qe->max_penalty))
+       if ((qe->max_penalty && (mem->penalty > qe->max_penalty)) || (qe->min_penalty && (mem->penalty < qe->min_penalty)))
                return -1;
 
        switch (q->strategy) {
@@ -3407,7 +3541,7 @@ static int remove_from_queue(const char *queuename, const char *interface)
                ao2_lock(q);
                if ((mem = ao2_find(q->members, &tmpmem, OBJ_POINTER))) {
                        /* XXX future changes should beware of this assumption!! */
-                       if(!mem->dynamic) {
+                       if (!mem->dynamic) {
                                ao2_ref(mem, -1);
                                ao2_unlock(q);
                                return RES_NOT_DYNAMIC;
@@ -3507,7 +3641,7 @@ static int set_member_paused(const char *queuename, const char *interface, const
                ast_queue_log("NONE", "NONE", interface, (paused ? "PAUSEALL" : "UNPAUSEALL"), "%s", "");
 
        queue_iter = ao2_iterator_init(queues, 0);
-       while((q = ao2_iterator_next(&queue_iter))) {
+       while ((q = ao2_iterator_next(&queue_iter))) {
                ao2_lock(q);
                if (ast_strlen_zero(queuename) || !strcasecmp(q->name, queuename)) {
                        if ((mem = interface_exists(q, interface))) {
@@ -3520,7 +3654,7 @@ static int set_member_paused(const char *queuename, const char *interface, const
                                if (queue_persistent_members)
                                        dump_queue_members(q);
 
-                               if(mem->realtime)
+                               if (mem->realtime)
                                        update_realtime_member_field(mem, q->name, "paused", paused ? "1" : "0");
 
                                ast_queue_log(q->name, "NONE", mem->membername, (paused ? "PAUSE" : "UNPAUSE"), "%s", S_OR(reason, ""));
@@ -3945,15 +4079,45 @@ static int ql_exec(struct ast_channel *chan, void *data)
        return 0;
 }
 
+static void copy_rules(struct queue_ent *qe, const char *rulename)
+{
+       struct penalty_rule *pr_iter;
+       struct rule_list *rl_iter;
+       const char *tmp = ast_strlen_zero(rulename) ? qe->parent->defaultrule : rulename;
+       AST_LIST_LOCK(&rule_lists);
+       AST_LIST_TRAVERSE(&rule_lists, rl_iter, list) {
+               if (!strcasecmp(rl_iter->name, tmp))
+                       break;
+       }
+       if (rl_iter) {
+               AST_LIST_TRAVERSE(&rl_iter->rules, pr_iter, list) {
+                       struct penalty_rule *new_pr = ast_calloc(1, sizeof(*new_pr));
+                       if (!new_pr) {
+                               ast_log(LOG_ERROR, "Memory allocation error when copying penalty rules! Aborting!\n");
+                               AST_LIST_UNLOCK(&rule_lists);
+                               break;
+                       }
+                       new_pr->time = pr_iter->time;
+                       new_pr->max_value = pr_iter->max_value;
+                       new_pr->min_value = pr_iter->min_value;
+                       new_pr->max_relative = pr_iter->max_relative;
+                       new_pr->min_relative = pr_iter->min_relative;
+                       AST_LIST_INSERT_TAIL(&qe->qe_rules, new_pr, list);
+               }
+       }
+       AST_LIST_UNLOCK(&rule_lists);
+}
+
 static int queue_exec(struct ast_channel *chan, void *data)
 {
        int res=-1;
        int ringing=0;
        const char *user_priority;
        const char *max_penalty_str;
+       const char *min_penalty_str;
        int prio;
        int qcontinue = 0;
-       int max_penalty;
+       int max_penalty, min_penalty;
        enum queue_result reason = QUEUE_UNKNOWN;
        /* whether to exit Queue application after the timeout hits */
        int tries = 0;
@@ -3969,6 +4133,7 @@ static int queue_exec(struct ast_channel *chan, void *data)
                AST_APP_ARG(agi);
                AST_APP_ARG(macro);
                AST_APP_ARG(gosub);
+               AST_APP_ARG(rule);
        );
        /* Our queue entry */
        struct queue_ent qe;
@@ -4007,6 +4172,7 @@ static int queue_exec(struct ast_channel *chan, void *data)
        }
 
        /* Get the maximum penalty from the variable ${QUEUE_MAX_PENALTY} */
+
        if ((max_penalty_str = pbx_builtin_getvar_helper(chan, "QUEUE_MAX_PENALTY"))) {
                if (sscanf(max_penalty_str, "%d", &max_penalty) == 1) {
                        ast_debug(1, "%s: Got max penalty %d from ${QUEUE_MAX_PENALTY}.\n", chan->name, max_penalty);
@@ -4019,6 +4185,18 @@ static int queue_exec(struct ast_channel *chan, void *data)
                max_penalty = 0;
        }
 
+       if ((min_penalty_str = pbx_builtin_getvar_helper(chan, "QUEUE_MIN_PENALTY"))) {
+               if (sscanf(min_penalty_str, "%d", &min_penalty) == 1) {
+                       ast_debug(1, "%s: Got min penalty %d from ${QUEUE_MIN_PENALTY}.\n", chan->name, min_penalty);
+               } else {
+                       ast_log(LOG_WARNING, "${QUEUE_MIN_PENALTY}: Invalid value (%s), channel %s.\n",
+                               min_penalty_str, chan->name);
+                       min_penalty = 0;
+               }
+       } else {
+               min_penalty = 0;
+       }
+
        if (args.options && (strchr(args.options, 'r')))
                ringing = 1;
 
@@ -4031,6 +4209,7 @@ static int queue_exec(struct ast_channel *chan, void *data)
        qe.chan = chan;
        qe.prio = prio;
        qe.max_penalty = max_penalty;
+       qe.min_penalty = min_penalty;
        qe.last_pos_said = 0;
        qe.last_pos = 0;
        qe.last_periodic_announce_time = time(NULL);
@@ -4039,6 +4218,8 @@ static int queue_exec(struct ast_channel *chan, void *data)
        if (!join_queue(args.queuename, &qe, &reason)) {
                ast_queue_log(args.queuename, chan->uniqueid, "NONE", "ENTERQUEUE", "%s|%s", S_OR(args.url, ""),
                        S_OR(chan->cid.cid_num, ""));
+               copy_rules(&qe, args.rule);
+               qe.pr = AST_LIST_FIRST(&qe.qe_rules);
 check_turns:
                if (ringing) {
                        ast_indicate(chan, AST_CONTROL_RINGING);
@@ -4091,7 +4272,7 @@ check_turns:
                                goto stop;
                        }
 
-                       stat = get_member_status(qe.parent, qe.max_penalty);
+                       stat = get_member_status(qe.parent, qe.max_penalty, qe.min_penalty);
 
                        /* exit after 'timeout' cycle if 'n' option enabled */
                        if (noption && tries >= qe.parent->membercount) {
@@ -4136,6 +4317,11 @@ check_turns:
                                break;
                        }
 
+                       /* see if we need to move to the next penalty level for this queue */
+                       while (qe.pr && ((time(NULL) - qe.start) > qe.pr->time)) {
+                               update_qe_rule(&qe);
+                       }
+
                        /* If using dynamic realtime members, we should regenerate the member list for this queue */
                        update_realtime_members(qe.parent);
 
@@ -4255,7 +4441,7 @@ static int queue_function_qac(struct ast_channel *chan, const char *cmd, char *d
                option = "logged";
        if ((q = load_realtime_queue(data))) {
                ao2_lock(q);
-               if(!strcasecmp(option, "logged")) {
+               if (!strcasecmp(option, "logged")) {
                        mem_iter = ao2_iterator_init(q->members, 0);
                        while ((m = ao2_iterator_next(&mem_iter))) {
                                /* Count the agents who are logged in and presently answering calls */
@@ -4264,7 +4450,7 @@ static int queue_function_qac(struct ast_channel *chan, const char *cmd, char *d
                                }
                                ao2_ref(m, -1);
                        }
-               } else if(!strcasecmp(option, "free")) {
+               } else if (!strcasecmp(option, "free")) {
                        mem_iter = ao2_iterator_init(q->members, 0);
                        while ((m = ao2_iterator_next(&mem_iter))) {
                                /* Count the agents who are logged in and presently answering calls */
@@ -4303,7 +4489,7 @@ static int queue_function_qac_dep(struct ast_channel *chan, const char *cmd, cha
                return -1;
        }
        
-       if((q = load_realtime_queue(data))) {
+       if ((q = load_realtime_queue(data))) {
                ao2_lock(q);
                mem_iter = ao2_iterator_init(q->members, 0);
                while ((m = ao2_iterator_next(&mem_iter))) {
@@ -4537,6 +4723,48 @@ static struct ast_custom_function queuememberpenalty_function = {
        .write = queue_function_memberpenalty_write,
 };
 
+static int reload_queue_rules(int reload)
+{
+       struct ast_config *cfg;
+       struct rule_list *rl_iter, *new_rl;
+       struct penalty_rule *pr_iter;
+       char *rulecat = NULL;
+       struct ast_variable *rulevar = NULL;
+       struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
+       
+       if (!(cfg = ast_config_load("queuerules.conf", config_flags))) {
+               ast_log(LOG_NOTICE, "No queuerules.conf file found, queues will not follow penalty rules\n");
+       } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) {
+               ast_log(LOG_NOTICE, "queuerules.conf has not changed since it was last loaded. Not taking any action.\n");
+               return AST_MODULE_LOAD_SUCCESS;
+       } else {
+               AST_LIST_LOCK(&rule_lists);
+               while ((rl_iter = AST_LIST_REMOVE_HEAD(&rule_lists, list))) {
+                       while ((pr_iter = AST_LIST_REMOVE_HEAD(&rl_iter->rules, list)))
+                               ast_free(pr_iter);
+                       ast_free(rl_iter);
+               }
+               while ((rulecat = ast_category_browse(cfg, rulecat))) {
+                       if (!(new_rl = ast_calloc(1, sizeof(*new_rl)))) {
+                               ast_log(LOG_ERROR, "Memory allocation error while loading queuerules.conf! Aborting!\n");
+                               AST_LIST_UNLOCK(&rule_lists);
+                               return AST_MODULE_LOAD_FAILURE;
+                       } else {
+                               ast_copy_string(new_rl->name, rulecat, sizeof(new_rl->name));
+                               AST_LIST_INSERT_TAIL(&rule_lists, new_rl, list);
+                               for (rulevar = ast_variable_browse(cfg, rulecat); rulevar; rulevar = rulevar->next)
+                                       if(!strcasecmp(rulevar->name, "penaltychange"))
+                                               insert_penaltychange(new_rl->name, rulevar->value, rulevar->lineno);
+                                       else
+                                               ast_log(LOG_WARNING, "Don't know how to handle rule type '%s' on line %d\n", rulevar->name, rulevar->lineno);
+                       }
+               }
+               AST_LIST_UNLOCK(&rule_lists);
+       }
+
+       return AST_MODULE_LOAD_SUCCESS;
+}
+
 
 static int reload_queues(int reload)
 {
@@ -4559,7 +4787,11 @@ static int reload_queues(int reload)
                AST_APP_ARG(penalty);
                AST_APP_ARG(membername);
        );
-       
+
+       /*First things first. Let's load queuerules.conf*/
+       if (reload_queue_rules(reload) == AST_MODULE_LOAD_FAILURE)
+               return AST_MODULE_LOAD_FAILURE;
+               
        if (!(cfg = ast_config_load("queues.conf", config_flags))) {
                ast_log(LOG_NOTICE, "No call queueing config file (queues.conf), so no call queues\n");
                return 0;
@@ -4570,7 +4802,7 @@ static int reload_queues(int reload)
        /* Mark all queues as dead for the moment */
        queue_iter = ao2_iterator_init(queues, F_AO2I_DONTLOCK);
        while ((q = ao2_iterator_next(&queue_iter))) {
-               if(!q->realtime) {
+               if (!q->realtime) {
                        q->dead = 1;
                        q->found = 0;
                }
@@ -4622,14 +4854,14 @@ static int reload_queues(int reload)
                                /* Check if a queue with this name already exists */
                                if (q->found) {
                                        ast_log(LOG_WARNING, "Queue '%s' already defined! Skipping!\n", cat);
-                                       if(!new)
+                                       if (!new)
                                                ao2_unlock(q);
                                        continue;
                                }
                                /* Due to the fact that the "linear" strategy will have a different allocation
                                 * scheme for queue members, we must devise the queue's strategy before other initializations
                                 */
-                               if((tmpvar = ast_variable_retrieve(cfg, cat, "strategy"))) {
+                               if ((tmpvar = ast_variable_retrieve(cfg, cat, "strategy"))) {
                                        q->strategy = strat2int(tmpvar);
                                        if (q->strategy < 0) {
                                                ast_log(LOG_WARNING, "'%s' isn't a valid strategy for queue '%s', using ringall instead\n",
@@ -4659,7 +4891,7 @@ static int reload_queues(int reload)
                                                AST_NONSTANDARD_APP_ARGS(args, parse, ',');
 
                                                interface = args.interface;
-                                               if(!ast_strlen_zero(args.penalty)) {
+                                               if (!ast_strlen_zero(args.penalty)) {
                                                        tmp = args.penalty;
                                                        while (*tmp && *tmp < 33) tmp++;
                                                        penalty = atoi(tmp);
@@ -4853,7 +5085,7 @@ static char *complete_queue(const char *line, const char *word, int pos, int sta
        struct ao2_iterator queue_iter;
 
        queue_iter = ao2_iterator_init(queues, 0);
-       while((q = ao2_iterator_next(&queue_iter))) {
+       while ((q = ao2_iterator_next(&queue_iter))) {
                if (!strncasecmp(word, q->name, wordlen) && ++which > state) {
                        ret = ast_strdup(q->name);
                        queue_unref(q);
@@ -4901,6 +5133,30 @@ static int manager_queues_show(struct mansession *s, const struct message *m)
        return RESULT_SUCCESS;
 }
 
+static int manager_queue_rule_show(struct mansession *s, const struct message *m)
+{
+       const char *rule = astman_get_header(m, "Rule");
+       struct rule_list *rl_iter;
+       struct penalty_rule *pr_iter;
+
+       AST_LIST_LOCK(&rule_lists);
+       AST_LIST_TRAVERSE(&rule_lists, rl_iter, list) {
+               if (ast_strlen_zero(rule) || !strcasecmp(rule, rl_iter->name)) {
+                       astman_append(s, "RuleList: %s\r\n", rl_iter->name);
+                       AST_LIST_TRAVERSE(&rl_iter->rules, pr_iter, list) {
+                               astman_append(s, "Rule: %d,%s%d,%s%d\r\n", pr_iter->time, pr_iter->max_relative && pr_iter->max_value >= 0 ? "+" : "", pr_iter->max_value, pr_iter->min_relative && pr_iter->min_value >= 0 ? "+" : "", pr_iter->min_value );
+                       }
+                       if (!ast_strlen_zero(rule))
+                               break;
+               }
+       }
+       AST_LIST_UNLOCK(&rule_lists);
+
+       astman_append(s, "\r\n\r\n");
+
+       return RESULT_SUCCESS;
+}
+
 /* Dump summary of queue info */
 static int manager_queues_summary(struct mansession *s, const struct message *m)
 {
@@ -4923,7 +5179,7 @@ static int manager_queues_summary(struct mansession *s, const struct message *m)
        if (!ast_strlen_zero(id))
                snprintf(idText, 256, "ActionID: %s\r\n", id);
        queue_iter = ao2_iterator_init(queues, 0);
-       while((q = ao2_iterator_next(&queue_iter))) {
+       while ((q = ao2_iterator_next(&queue_iter))) {
                ao2_lock(q);
 
                /* List queue properties */
@@ -5452,19 +5708,19 @@ static char *handle_queue_pause_member(struct ast_cli_entry *e, int cmd, struct
        reason = a->argc == 8 ? a->argv[7] : NULL;
        paused = !strcasecmp(a->argv[1], "pause");
 
-       if(set_member_paused(queuename, interface, reason, paused) == RESULT_SUCCESS) {
+       if (set_member_paused(queuename, interface, reason, paused) == RESULT_SUCCESS) {
                ast_cli(a->fd, "%spaused interface '%s'", paused ? "" : "un", interface);
-               if(!ast_strlen_zero(queuename))
+               if (!ast_strlen_zero(queuename))
                        ast_cli(a->fd, " in queue '%s'", queuename);
-               if(!ast_strlen_zero(reason))
+               if (!ast_strlen_zero(reason))
                        ast_cli(a->fd, " for reason '%s'", reason);
                ast_cli(a->fd, "\n");
                return CLI_SUCCESS;
        } else {
                ast_cli(a->fd, "Unable to %spause interface '%s'", paused ? "" : "un", interface);
-               if(!ast_strlen_zero(queuename))
+               if (!ast_strlen_zero(queuename))
                        ast_cli(a->fd, " in queue '%s'", queuename);
-               if(!ast_strlen_zero(reason))
+               if (!ast_strlen_zero(reason))
                        ast_cli(a->fd, " for reason '%s'", reason);
                ast_cli(a->fd, "\n");
                return CLI_FAILURE;
@@ -5517,7 +5773,7 @@ static char *handle_queue_set_member_penalty(struct ast_cli_entry *e, int cmd, s
                return CLI_SHOWUSAGE;
        }
 
-       if(a->argc == 8)
+       if (a->argc == 8)
                queuename = a->argv[7];
        interface = a->argv[5];
        penalty = atoi(a->argv[3]);
@@ -5534,6 +5790,79 @@ static char *handle_queue_set_member_penalty(struct ast_cli_entry *e, int cmd, s
        }
 }
 
+static char *complete_queue_rule_show(const char *line, const char *word, int pos, int state) 
+{
+       int which = 0;
+       struct rule_list *rl_iter;
+       int wordlen = strlen(word);
+       char *ret = NULL;
+       if (pos != 3) /* Wha? */ {
+               ast_log(LOG_DEBUG, "Hitting this???, pos is %d\n", pos);
+               return NULL;
+       }
+
+       AST_LIST_LOCK(&rule_lists);
+       AST_LIST_TRAVERSE(&rule_lists, rl_iter, list) {
+               if (!strncasecmp(word, rl_iter->name, wordlen) && ++which > state) {
+                       ret = ast_strdup(rl_iter->name);
+                       break;
+               }
+       }
+       AST_LIST_UNLOCK(&rule_lists);
+
+       return ret;
+}
+
+static char *handle_queue_rule_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+       char *rule;
+       struct rule_list *rl_iter;
+       struct penalty_rule *pr_iter;
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "queue rules show";
+               e->usage =
+               "Usage: queue rules show [rulename]\n"
+               "Show the list of rules associated with rulename. If no\n"
+               "rulename is specified, list all rules defined in queuerules.conf\n";
+               return NULL;
+       case CLI_GENERATE:
+               return complete_queue_rule_show(a->line, a->word, a->pos, a->n);
+       }
+
+       if (a->argc != 3 && a->argc != 4)
+               return CLI_SHOWUSAGE;
+
+       rule = a->argc == 4 ? a->argv[3] : "";
+       AST_LIST_LOCK(&rule_lists);
+       AST_LIST_TRAVERSE(&rule_lists, rl_iter, list) {
+               if (ast_strlen_zero(rule) || !strcasecmp(rl_iter->name, rule)) {
+                       ast_cli(a->fd, "Rule: %s\n", rl_iter->name);
+                       AST_LIST_TRAVERSE(&rl_iter->rules, pr_iter, list) {
+                               ast_cli(a->fd, "\tAfter %d seconds, adjust QUEUE_MAX_PENALTY %s %d and adjust QUEUE_MIN_PENALTY %s %d\n", pr_iter->time, pr_iter->max_relative ? "by" : "to", pr_iter->max_value, pr_iter->min_relative ? "by" : "to", pr_iter->min_value);
+                       }
+               }
+       }
+       AST_LIST_UNLOCK(&rule_lists);
+       return CLI_SUCCESS; 
+}
+
+static char *handle_queue_rule_reload(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+       switch (cmd) {
+               case CLI_INIT:
+                       e->command = "queue rules reload";
+                       e->usage = 
+                               "Usage: queue rules reload\n"
+                               "Reloads rules defined in queuerules.conf\n";
+                       return NULL;
+               case CLI_GENERATE:
+                       return NULL;
+       }
+       reload_queue_rules(1);
+       return CLI_SUCCESS;
+}
+
 static const char qpm_cmd_usage[] = 
 "Usage: queue pause member <channel> in <queue> reason <reason>\n";
 
@@ -5549,6 +5878,8 @@ static struct ast_cli_entry cli_queue[] = {
        AST_CLI_DEFINE(handle_queue_remove_member, "Removes a channel from a specified queue"),
        AST_CLI_DEFINE(handle_queue_pause_member, "Pause or unpause a queue member"),
        AST_CLI_DEFINE(handle_queue_set_member_penalty, "Set penalty for a channel of a specified queue"),
+       AST_CLI_DEFINE(handle_queue_rule_show, "Show the rules defined in queuerules.conf"),
+       AST_CLI_DEFINE(handle_queue_rule_reload, "Reload the rules defined in queuerules.conf"),
 };
 
 static int unload_module(void)
@@ -5642,6 +5973,7 @@ static int load_module(void)
        res |= ast_manager_register("QueuePause", EVENT_FLAG_AGENT, manager_pause_queue_member, "Makes a queue member temporarily unavailable");
        res |= ast_manager_register("QueueLog", EVENT_FLAG_AGENT, manager_queue_log_custom, "Adds custom entry in queue_log");
        res |= ast_manager_register("QueuePenalty", EVENT_FLAG_AGENT, manager_queue_member_penalty, "Set the penalty for a queue member"); 
+       res |= ast_manager_register("QueueRule", 0, manager_queue_rule_show, "Queue Rules");
        res |= ast_custom_function_register(&queuevar_function);
        res |= ast_custom_function_register(&queuemembercount_function);
        res |= ast_custom_function_register(&queuemembercount_dep);
diff --git a/configs/queuerules.conf.sample b/configs/queuerules.conf.sample
new file mode 100644 (file)
index 0000000..5ab794b
--- /dev/null
@@ -0,0 +1,20 @@
+; It is possible to change the value of the QUEUE_MAX_PENALTY and QUEUE_MIN_PENALTY 
+; channel variables in mid-call by defining rules in the queue for when to do so. This can allow for
+; a call to be opened to more members or potentially a different set of members. 
+; The advantage to changing members this way as opposed to inserting the caller into a 
+; different queue with more members or reinserting the caller into the same queue with a different 
+; QUEUE_MAX_PENALTY or QUEUE_MIN_PENALTY set is that the caller does not lose his place in the queue. 
+;
+; Note: There is a limitation to these rules; a caller will follow the penaltychange rules for 
+; the queue that were defined at the time the caller entered the queue. If an update to the rules is 
+; made during the the caller's stay in the queue, these will not be reflected for that caller.
+;
+; The syntax for these rules is
+; penaltychange => <number of seconds into the call>,<absolute or relative change to QUEUE_MAX_PENALTY>[,absolute or relative change to QUEUE_MIN_PENALTY]
+;
+; Example:
+; [myrule]
+; penaltychange => 30,+3   ; 30 seconds into the call increase the QUEUE_MAX_PENALTY by 3, no change to QUEUE_MIN_PENALTY
+; penaltychange => 60,10,5 ; 60 seconds into the call increase the QUEUE_MAX_PENALTY to 10 and increase the QUEUE_MIN_PENALTY to 5
+; penaltychange => 75,,7   ; 75 seconds into the call keep the QUEUE_MAX_PENALTY the same and increase the QUEUE_MIN_PENALTY to 7
+
index 440da31..7fe1486 100644 (file)
@@ -362,6 +362,13 @@ shared_lastcall=no
 ;
 ; timeoutrestart = no
 ;
+; If you wish to implement a rule defined in queuerules.conf (see 
+; configs/queuerules.conf.sample from the asterisk source directory for
+; more information about penalty rules) by default, you may specify this
+; by setting defaultrule to the rule's name
+;
+; defaultrule = myrule
+;
 ; Each member of this call queue is listed on a separate line in
 ; the form technology/dialstring.  "member" means a normal member of a
 ; queue.  An optional penalty may be specified after a comma, such that
index 1d2bc82..d6981ec 100644 (file)
@@ -826,6 +826,7 @@ ${MONITOR_EXEC_ARGS}      Arguments to application
 ${MONITOR_FILENAME}       File for monitoring (recording) calls in queue
 ${QUEUE_PRIO}             Queue priority
 ${QUEUE_MAX_PENALTY}      Maximum member penalty allowed to answer caller
+${QUEUE_MIN_PENALTY}      Minimum member penalty allowed to answer caller
 ${QUEUESTATUS}            Status of the call, one of:
                           (TIMEOUT | FULL | JOINEMPTY | LEAVEEMPTY | JOINUNAVAIL | LEAVEUNAVAIL)
 ${RECORDED_FILE}        * Recorded file in record()