Replace most uses of ast_register_atexit with ast_register_cleanup.
[asterisk/asterisk.git] / main / cdr.c
index 1677b1b..5e24dae 100644 (file)
  * isn't properly generated and posted.
  */
 
+/*! \li \ref cdr.c uses the configuration file \ref cdr.conf
+ * \addtogroup configuration_file Configuration Files
+ */
+
+/*!
+ * \page cdr.conf cdr.conf
+ * \verbinclude cdr.conf.sample
+ */
+
+/*** MODULEINFO
+       <support_level>core</support_level>
+ ***/
 
 #include "asterisk.h"
 
 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 
 #include <signal.h>
+#include <inttypes.h>
 
 #include "asterisk/lock.h"
 #include "asterisk/channel.h"
@@ -50,912 +63,3116 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 #include "asterisk/cli.h"
 #include "asterisk/stringfields.h"
 #include "asterisk/data.h"
+#include "asterisk/config_options.h"
+#include "asterisk/json.h"
+#include "asterisk/parking.h"
+#include "asterisk/stasis.h"
+#include "asterisk/stasis_channels.h"
+#include "asterisk/stasis_bridges.h"
+#include "asterisk/stasis_message_router.h"
+#include "asterisk/astobj2.h"
+
+/*** DOCUMENTATION
+       <configInfo name="cdr" language="en_US">
+               <synopsis>Call Detail Record configuration</synopsis>
+               <description>
+                       <para>CDR is Call Detail Record, which provides logging services via a variety of
+                       pluggable backend modules. Detailed call information can be recorded to
+                       databases, files, etc. Useful for billing, fraud prevention, compliance with
+                       Sarbanes-Oxley aka The Enron Act, QOS evaluations, and more.</para>
+               </description>
+               <configFile name="cdr.conf">
+                       <configObject name="general">
+                               <synopsis>Global settings applied to the CDR engine.</synopsis>
+                               <configOption name="debug">
+                                       <synopsis>Enable/disable verbose CDR debugging.</synopsis>
+                                       <description><para>When set to <literal>True</literal>, verbose updates
+                                       of changes in CDR information will be logged. Note that this is only
+                                       of use when debugging CDR behavior.</para>
+                                       </description>
+                               </configOption>
+                               <configOption name="enable">
+                                       <synopsis>Enable/disable CDR logging.</synopsis>
+                                       <description><para>Define whether or not to use CDR logging. Setting this to "no" will override
+                                       any loading of backend CDR modules.  Default is "yes".</para>
+                                       </description>
+                               </configOption>
+                               <configOption name="unanswered">
+                                       <synopsis>Log calls that are never answered and don't set an outgoing party.</synopsis>
+                                       <description><para>
+                                       Define whether or not to log unanswered calls that don't involve an outgoing party. Setting
+                                       this to "yes" will make calls to extensions that don't answer and don't set a side B channel
+                                       (such as by using the Dial application) receive CDR log entries. If this option is set to
+                                       "no", then those log entries will not be created. Unanswered calls which get offered to an
+                                       outgoing line will always receive log entries regardless of this option, and that is the
+                                       intended behavior.
+                                       </para>
+                                       </description>
+                               </configOption>
+                               <configOption name="congestion">
+                                       <synopsis>Log congested calls.</synopsis>
+                                       <description><para>Define whether or not to log congested calls. Setting this to "yes" will
+                                       report each call that fails to complete due to congestion conditions.</para>
+                                       </description>
+                               </configOption>
+                               <configOption name="endbeforehexten">
+                                       <synopsis>Don't produce CDRs while executing hangup logic</synopsis>
+                                       <description>
+                                               <para>As each CDR for a channel is finished, its end time is updated
+                                               and the CDR is finalized. When a channel is hung up and hangup
+                                               logic is present (in the form of a hangup handler or the
+                                               <literal>h</literal> extension), a new CDR is generated for the
+                                               channel. Any statistics are gathered from this new CDR. By enabling
+                                               this option, no new CDR is created for the dialplan logic that is
+                                               executed in <literal>h</literal> extensions or attached hangup handler
+                                               subroutines. The default value is <literal>yes</literal>, indicating
+                                               that a CDR will be generated during hangup logic.</para>
+                                       </description>
+                               </configOption>
+                               <configOption name="initiatedseconds">
+                                       <synopsis>Count microseconds for billsec purposes</synopsis>
+                                       <description><para>Normally, the <literal>billsec</literal> field logged to the CDR backends
+                                       is simply the end time (hangup time) minus the answer time in seconds. Internally,
+                                       asterisk stores the time in terms of microseconds and seconds. By setting
+                                       initiatedseconds to <literal>yes</literal>, you can force asterisk to report any seconds
+                                       that were initiated (a sort of round up method). Technically, this is
+                                       when the microsecond part of the end time is greater than the microsecond
+                                       part of the answer time, then the billsec time is incremented one second.</para>
+                                       </description>
+                               </configOption>
+                               <configOption name="batch">
+                                       <synopsis>Submit CDRs to the backends for processing in batches</synopsis>
+                                       <description><para>Define the CDR batch mode, where instead of posting the CDR at the end of
+                                       every call, the data will be stored in a buffer to help alleviate load on the
+                                       asterisk server.</para>
+                                       <warning><para>Use of batch mode may result in data loss after unsafe asterisk termination,
+                                       i.e., software crash, power failure, kill -9, etc.</para>
+                                       </warning>
+                                       </description>
+                               </configOption>
+                               <configOption name="size">
+                                       <synopsis>The maximum number of CDRs to accumulate before triggering a batch</synopsis>
+                                       <description><para>Define the maximum number of CDRs to accumulate in the buffer before posting
+                                       them to the backend engines. batch must be set to <literal>yes</literal>.</para>
+                                       </description>
+                               </configOption>
+                               <configOption name="time">
+                                       <synopsis>The maximum time to accumulate CDRs before triggering a batch</synopsis>
+                                       <description><para>Define the maximum time to accumulate CDRs before posting them in a batch to the
+                                       backend engines. If this time limit is reached, then it will post the records, regardless of the value
+                                       defined for size. batch must be set to <literal>yes</literal>.</para>
+                                       <note><para>Time is expressed in seconds.</para></note>
+                                       </description>
+                               </configOption>
+                               <configOption name="scheduleronly">
+                                       <synopsis>Post batched CDRs on their own thread instead of the scheduler</synopsis>
+                                       <description><para>The CDR engine uses the internal asterisk scheduler to determine when to post
+                                       records.  Posting can either occur inside the scheduler thread, or a new
+                                       thread can be spawned for the submission of every batch.  For small batches,
+                                       it might be acceptable to just use the scheduler thread, so set this to <literal>yes</literal>.
+                                       For large batches, say anything over size=10, a new thread is recommended, so
+                                       set this to <literal>no</literal>.</para>
+                                       </description>
+                               </configOption>
+                               <configOption name="safeshutdown">
+                                       <synopsis>Block shutdown of Asterisk until CDRs are submitted</synopsis>
+                                       <description><para>When shutting down asterisk, you can block until the CDRs are submitted.  If
+                                       you don't, then data will likely be lost.  You can always check the size of
+                                       the CDR batch buffer with the CLI <astcli>cdr status</astcli> command.  To enable blocking on
+                                       submission of CDR data during asterisk shutdown, set this to <literal>yes</literal>.</para>
+                                       </description>
+                               </configOption>
+                       </configObject>
+               </configFile>
+       </configInfo>
+ ***/
+
+
+/* The prime here should be similar in size to the channel container. */
+#ifdef LOW_MEMORY
+#define NUM_CDR_BUCKETS 61
+#else
+#define NUM_CDR_BUCKETS 769
+#endif
+
+#define DEFAULT_ENABLED "1"
+#define DEFAULT_BATCHMODE "0"
+#define DEFAULT_UNANSWERED "0"
+#define DEFAULT_CONGESTION "0"
+#define DEFAULT_END_BEFORE_H_EXTEN "1"
+#define DEFAULT_INITIATED_SECONDS "0"
+
+#define DEFAULT_BATCH_SIZE "100"
+#define MAX_BATCH_SIZE 1000
+#define DEFAULT_BATCH_TIME "300"
+#define MAX_BATCH_TIME 86400
+#define DEFAULT_BATCH_SCHEDULER_ONLY "0"
+#define DEFAULT_BATCH_SAFE_SHUTDOWN "1"
+
+#define CDR_DEBUG(mod_cfg, fmt, ...) \
+       do { \
+               if (ast_test_flag(&(mod_cfg)->general->settings, CDR_DEBUG)) { \
+                       ast_verbose((fmt), ##__VA_ARGS__); \
+               } \
+       } while (0)
+
+static void cdr_detach(struct ast_cdr *cdr);
+static void cdr_submit_batch(int shutdown);
+static int cdr_toggle_runtime_options(void);
+
+/*! \brief The configuration settings for this module */
+struct module_config {
+       struct ast_cdr_config *general;         /*< CDR global settings */
+};
+
+/*! \brief The container for the module configuration */
+static AO2_GLOBAL_OBJ_STATIC(module_configs);
+
+/*! \brief The type definition for general options */
+static struct aco_type general_option = {
+       .type = ACO_GLOBAL,
+       .name = "general",
+       .item_offset = offsetof(struct module_config, general),
+       .category = "^general$",
+       .category_match = ACO_WHITELIST,
+};
+
+static void *module_config_alloc(void);
+static void module_config_destructor(void *obj);
+
+/*! \brief The file definition */
+static struct aco_file module_file_conf = {
+       .filename = "cdr.conf",
+       .skip_category = "(^csv$|^custom$|^manager$|^odbc$|^pgsql$|^radius$|^sqlite$|^tds$|^mysql$)",
+       .types = ACO_TYPES(&general_option),
+};
+
+CONFIG_INFO_CORE("cdr", cfg_info, module_configs, module_config_alloc,
+       .files = ACO_FILES(&module_file_conf),
+);
+
+static struct aco_type *general_options[] = ACO_TYPES(&general_option);
+
+/*! \brief Dispose of a module config object */
+static void module_config_destructor(void *obj)
+{
+       struct module_config *cfg = obj;
+
+       if (!cfg) {
+               return;
+       }
+       ao2_ref(cfg->general, -1);
+}
+
+/*! \brief Create a new module config object */
+static void *module_config_alloc(void)
+{
+       struct module_config *mod_cfg;
+       struct ast_cdr_config *cdr_config;
+
+       mod_cfg = ao2_alloc(sizeof(*mod_cfg), module_config_destructor);
+       if (!mod_cfg) {
+               return NULL;
+       }
 
-/*! Default AMA flag for billing records (CDR's) */
-int ast_default_amaflags = AST_CDR_DOCUMENTATION;
-char ast_default_accountcode[AST_MAX_ACCOUNT_CODE];
+       cdr_config = ao2_alloc(sizeof(*cdr_config), NULL);
+       if (!cdr_config) {
+               ao2_ref(cdr_config, -1);
+               return NULL;
+       }
+       mod_cfg->general = cdr_config;
+
+       return mod_cfg;
+}
 
-struct ast_cdr_beitem {
+/*! \brief Registration object for CDR backends */
+struct cdr_beitem {
        char name[20];
        char desc[80];
        ast_cdrbe be;
-       AST_RWLIST_ENTRY(ast_cdr_beitem) list;
+       AST_RWLIST_ENTRY(cdr_beitem) list;
+       int suspended:1;
 };
 
-static AST_RWLIST_HEAD_STATIC(be_list, ast_cdr_beitem);
+/*! \brief List of registered backends */
+static AST_RWLIST_HEAD_STATIC(be_list, cdr_beitem);
 
-struct ast_cdr_batch_item {
+/*! \brief Queued CDR waiting to be batched */
+struct cdr_batch_item {
        struct ast_cdr *cdr;
-       struct ast_cdr_batch_item *next;
+       struct cdr_batch_item *next;
 };
 
-static struct ast_cdr_batch {
+/*! \brief The actual batch queue */
+static struct cdr_batch {
        int size;
-       struct ast_cdr_batch_item *head;
-       struct ast_cdr_batch_item *tail;
+       struct cdr_batch_item *head;
+       struct cdr_batch_item *tail;
 } *batch = NULL;
 
+/*! \brief The global sequence counter used for CDRs */
+static int global_cdr_sequence =  0;
 
-static int cdr_sequence =  0;
-
-static int cdr_seq_inc(struct ast_cdr *cdr);
-
+/*! \brief Scheduler items */
 static struct ast_sched_context *sched;
 static int cdr_sched = -1;
+AST_MUTEX_DEFINE_STATIC(cdr_sched_lock);
 static pthread_t cdr_thread = AST_PTHREADT_NULL;
 
-static int enabled;
-static const int ENABLED_DEFAULT = 1;
+/*! \brief Lock protecting modifications to the batch queue */
+AST_MUTEX_DEFINE_STATIC(cdr_batch_lock);
 
-static int batchmode;
-static const int BATCHMODE_DEFAULT = 0;
+/*! \brief These are used to wake up the CDR thread when there's work to do */
+AST_MUTEX_DEFINE_STATIC(cdr_pending_lock);
+static ast_cond_t cdr_pending_cond;
 
-static int unanswered;
-static const int UNANSWERED_DEFAULT = 0;
+/*! \brief A container of the active CDRs indexed by Party A channel id */
+static struct ao2_container *active_cdrs_by_channel;
+
+/*! \brief Message router for stasis messages regarding channel state */
+static struct stasis_message_router *stasis_router;
+
+/*! \brief Our subscription for bridges */
+static struct stasis_forward *bridge_subscription;
+
+/*! \brief Our subscription for channels */
+static struct stasis_forward *channel_subscription;
+
+/*! \brief Our subscription for parking */
+static struct stasis_forward *parking_subscription;
+
+/*! \brief The parent topic for all topics we want to aggregate for CDRs */
+static struct stasis_topic *cdr_topic;
+
+/*! \brief A message type used to synchronize with the CDR topic */
+STASIS_MESSAGE_TYPE_DEFN_LOCAL(cdr_sync_message_type);
+
+struct cdr_object;
+
+/*! \brief Return types for \ref process_bridge_enter functions */
+enum process_bridge_enter_results {
+       /*!
+        * The CDR was the only party in the bridge.
+        */
+       BRIDGE_ENTER_ONLY_PARTY,
+       /*!
+        * The CDR was able to obtain a Party B from some other party already in the bridge
+        */
+       BRIDGE_ENTER_OBTAINED_PARTY_B,
+       /*!
+        * The CDR was not able to obtain a Party B
+        */
+       BRIDGE_ENTER_NO_PARTY_B,
+       /*!
+        * This CDR can't handle a bridge enter message and a new CDR needs to be created
+        */
+       BRIDGE_ENTER_NEED_CDR,
+};
 
-static int batchsize;
-static const int BATCH_SIZE_DEFAULT = 100;
+/*!
+ * \brief A virtual table used for \ref cdr_object.
+ *
+ * Note that all functions are optional - if a subclass does not need an
+ * implementation, it is safe to leave it NULL.
+ */
+struct cdr_object_fn_table {
+       /*! \brief Name of the subclass */
+       const char *name;
+
+       /*!
+        * \brief An initialization function. This will be called automatically
+        * when a \ref cdr_object is switched to this type in
+        * \ref cdr_object_transition_state
+        *
+        * \param cdr The \ref cdr_object that was just transitioned
+        */
+       void (* const init_function)(struct cdr_object *cdr);
+
+       /*!
+        * \brief Process a Party A update for the \ref cdr_object
+        *
+        * \param cdr The \ref cdr_object to process the update
+        * \param snapshot The snapshot for the CDR's Party A
+        * \retval 0 the CDR handled the update or ignored it
+        * \retval 1 the CDR is finalized and a new one should be made to handle it
+        */
+       int (* const process_party_a)(struct cdr_object *cdr,
+                       struct ast_channel_snapshot *snapshot);
+
+       /*!
+        * \brief Process a Party B update for the \ref cdr_object
+        *
+        * \param cdr The \ref cdr_object to process the update
+        * \param snapshot The snapshot for the CDR's Party B
+        */
+       void (* const process_party_b)(struct cdr_object *cdr,
+                       struct ast_channel_snapshot *snapshot);
+
+       /*!
+        * \brief Process the beginning of a dial. A dial message implies one of two
+        * things:
+        * The \ref cdr_object's Party A has been originated
+        * The \ref cdr_object's Party A is dialing its Party B
+        *
+        * \param cdr The \ref cdr_object
+        * \param caller The originator of the dial attempt
+        * \param peer The destination of the dial attempt
+        *
+        * \retval 0 if the parties in the dial were handled by this CDR
+        * \retval 1 if the parties could not be handled by this CDR
+        */
+       int (* const process_dial_begin)(struct cdr_object *cdr,
+                       struct ast_channel_snapshot *caller,
+                       struct ast_channel_snapshot *peer);
+
+       /*!
+        * \brief Process the end of a dial. At the end of a dial, a CDR can be
+        * transitioned into one of two states - DialedPending
+        * (\ref dialed_pending_state_fn_table) or Finalized
+        * (\ref finalized_state_fn_table).
+        *
+        * \param cdr The \ref cdr_object
+        * \param caller The originator of the dial attempt
+        * \param peer the Destination of the dial attempt
+        * \param dial_status What happened
+        *
+        * \retval 0 if the parties in the dial were handled by this CDR
+        * \retval 1 if the parties could not be handled by this CDR
+        */
+       int (* const process_dial_end)(struct cdr_object *cdr,
+                       struct ast_channel_snapshot *caller,
+                       struct ast_channel_snapshot *peer,
+                       const char *dial_status);
+
+       /*!
+        * \brief Process the entering of a bridge by this CDR. The purpose of this
+        * callback is to have the CDR prepare itself for the bridge and attempt to
+        * find a valid Party B. The act of creating new CDRs based on the entering
+        * of this channel into the bridge is handled by the higher level message
+        * handler.
+        *
+        * Note that this handler is for when a channel enters into a "normal"
+        * bridge, where people actually talk to each other. Parking is its own
+        * thing.
+        *
+        * \param cdr The \ref cdr_object
+        * \param bridge The bridge that the Party A just entered into
+        * \param channel The \ref ast_channel_snapshot for this CDR's Party A
+        *
+        * \retval process_bridge_enter_results Defines whether or not this CDR was able
+        * to fully handle the bridge enter message.
+        */
+       enum process_bridge_enter_results (* const process_bridge_enter)(
+                       struct cdr_object *cdr,
+                       struct ast_bridge_snapshot *bridge,
+                       struct ast_channel_snapshot *channel);
+
+       /*!
+        * \brief Process entering into a parking bridge.
+        *
+        * \param cdr The \ref cdr_object
+        * \param bridge The parking bridge that Party A just entered into
+        * \param channel The \ref ast_channel_snapshot for this CDR's Party A
+        *
+        * \retval 0 This CDR successfully transitioned itself into the parked state
+        * \retval 1 This CDR couldn't handle the parking transition and we need a
+        *  new CDR.
+        */
+       int (* const process_parking_bridge_enter)(struct cdr_object *cdr,
+                       struct ast_bridge_snapshot *bridge,
+                       struct ast_channel_snapshot *channel);
+
+       /*!
+        * \brief Process the leaving of a bridge by this CDR.
+        *
+        * \param cdr The \ref cdr_object
+        * \param bridge The bridge that the Party A just left
+        * \param channel The \ref ast_channel_snapshot for this CDR's Party A
+        *
+        * \retval 0 This CDR left successfully
+        * \retval 1 Error
+        */
+       int (* const process_bridge_leave)(struct cdr_object *cdr,
+                       struct ast_bridge_snapshot *bridge,
+                       struct ast_channel_snapshot *channel);
+
+       /*!
+        * \brief Process an update informing us that the channel got itself parked
+        *
+        * \param cdr The \ref cdr_object
+        * \param channel The parking information for this CDR's party A
+        *
+        * \retval 0 This CDR successfully parked itself
+        * \retval 1 This CDR couldn't handle the park
+        */
+       int (* const process_parked_channel)(struct cdr_object *cdr,
+                       struct ast_parked_call_payload *parking_info);
+};
 
-static int batchtime;
-static const int BATCH_TIME_DEFAULT = 300;
+static int base_process_party_a(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot);
+static enum process_bridge_enter_results base_process_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel);
+static int base_process_bridge_leave(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel);
+static int base_process_dial_end(struct cdr_object *cdr, struct ast_channel_snapshot *caller, struct ast_channel_snapshot *peer, const char *dial_status);
+static int base_process_parked_channel(struct cdr_object *cdr, struct ast_parked_call_payload *parking_info);
 
-static int batchscheduleronly;
-static const int BATCH_SCHEDULER_ONLY_DEFAULT = 0;
+static void single_state_init_function(struct cdr_object *cdr);
+static void single_state_process_party_b(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot);
+static int single_state_process_dial_begin(struct cdr_object *cdr, struct ast_channel_snapshot *caller, struct ast_channel_snapshot *peer);
+static enum process_bridge_enter_results single_state_process_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel);
+static int single_state_process_parking_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel);
 
-static int batchsafeshutdown;
-static const int BATCH_SAFE_SHUTDOWN_DEFAULT = 1;
+/*!
+ * \brief The virtual table for the Single state.
+ *
+ * A \ref cdr_object starts off in this state. This represents a channel that
+ * has no Party B information itself.
+ *
+ * A \ref cdr_object from this state can go into any of the following states:
+ * * \ref dial_state_fn_table
+ * * \ref bridge_state_fn_table
+ * * \ref finalized_state_fn_table
+ */
+struct cdr_object_fn_table single_state_fn_table = {
+       .name = "Single",
+       .init_function = single_state_init_function,
+       .process_party_a = base_process_party_a,
+       .process_party_b = single_state_process_party_b,
+       .process_dial_begin = single_state_process_dial_begin,
+       .process_dial_end = base_process_dial_end,
+       .process_bridge_enter = single_state_process_bridge_enter,
+       .process_parking_bridge_enter = single_state_process_parking_bridge_enter,
+       .process_bridge_leave = base_process_bridge_leave,
+       .process_parked_channel = base_process_parked_channel,
+};
 
-AST_MUTEX_DEFINE_STATIC(cdr_batch_lock);
+static void dial_state_process_party_b(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot);
+static int dial_state_process_dial_begin(struct cdr_object *cdr, struct ast_channel_snapshot *caller, struct ast_channel_snapshot *peer);
+static int dial_state_process_dial_end(struct cdr_object *cdr, struct ast_channel_snapshot *caller, struct ast_channel_snapshot *peer, const char *dial_status);
+static enum process_bridge_enter_results dial_state_process_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel);
 
-/* these are used to wake up the CDR thread when there's work to do */
-AST_MUTEX_DEFINE_STATIC(cdr_pending_lock);
-static ast_cond_t cdr_pending_cond;
+/*!
+ * \brief The virtual table for the Dial state.
+ *
+ * A \ref cdr_object that has begun a dial operation. This state is entered when
+ * the Party A for a CDR is determined to be dialing out to a Party B or when
+ * a CDR is for an originated channel (in which case the Party A information is
+ * the originated channel, and there is no Party B).
+ *
+ * A \ref cdr_object from this state can go in any of the following states:
+ * * \ref dialed_pending_state_fn_table
+ * * \ref bridge_state_fn_table
+ * * \ref finalized_state_fn_table
+ */
+struct cdr_object_fn_table dial_state_fn_table = {
+       .name = "Dial",
+       .process_party_a = base_process_party_a,
+       .process_party_b = dial_state_process_party_b,
+       .process_dial_begin = dial_state_process_dial_begin,
+       .process_dial_end = dial_state_process_dial_end,
+       .process_bridge_enter = dial_state_process_bridge_enter,
+       .process_bridge_leave = base_process_bridge_leave,
+};
 
-int check_cdr_enabled()
-{
-       return enabled;
-}
+static int dialed_pending_state_process_party_a(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot);
+static int dialed_pending_state_process_dial_begin(struct cdr_object *cdr, struct ast_channel_snapshot *caller, struct ast_channel_snapshot *peer);
+static enum process_bridge_enter_results dialed_pending_state_process_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel);
+static int dialed_pending_state_process_parking_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel);
 
-/*! Register a CDR driver. Each registered CDR driver generates a CDR
-       \return 0 on success, -1 on failure
-*/
-int ast_cdr_register(const char *name, const char *desc, ast_cdrbe be)
-{
-       struct ast_cdr_beitem *i = NULL;
+/*!
+ * \brief The virtual table for the Dialed Pending state.
+ *
+ * A \ref cdr_object that has successfully finished a dial operation, but we
+ * don't know what they're going to do yet. It's theoretically possible to dial
+ * a party and then have that party not be bridged with the caller; likewise,
+ * an origination can complete and the channel go off and execute dialplan. The
+ * pending state acts as a bridge between either:
+ * * Entering a bridge
+ * * Getting a new CDR for new dialplan execution
+ * * Switching from being originated to executing dialplan
+ *
+ * A \ref cdr_object from this state can go in any of the following states:
+ * * \ref single_state_fn_table
+ * * \ref dialed_pending_state_fn_table
+ * * \ref bridge_state_fn_table
+ * * \ref finalized_state_fn_table
+ */
+struct cdr_object_fn_table dialed_pending_state_fn_table = {
+       .name = "DialedPending",
+       .process_party_a = dialed_pending_state_process_party_a,
+       .process_dial_begin = dialed_pending_state_process_dial_begin,
+       .process_bridge_enter = dialed_pending_state_process_bridge_enter,
+       .process_parking_bridge_enter = dialed_pending_state_process_parking_bridge_enter,
+       .process_bridge_leave = base_process_bridge_leave,
+       .process_parked_channel = base_process_parked_channel,
+};
 
-       if (!name)
-               return -1;
+static void bridge_state_process_party_b(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot);
+static int bridge_state_process_bridge_leave(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel);
 
-       if (!be) {
-               ast_log(LOG_WARNING, "CDR engine '%s' lacks backend\n", name);
-               return -1;
-       }
+/*!
+ * \brief The virtual table for the Bridged state
+ *
+ * A \ref cdr_object enters this state when it receives notification that the
+ * channel has entered a bridge.
+ *
+ * A \ref cdr_object from this state can go to:
+ * * \ref finalized_state_fn_table
+ */
+struct cdr_object_fn_table bridge_state_fn_table = {
+       .name = "Bridged",
+       .process_party_a = base_process_party_a,
+       .process_party_b = bridge_state_process_party_b,
+       .process_bridge_leave = bridge_state_process_bridge_leave,
+       .process_parked_channel = base_process_parked_channel,
+};
 
-       AST_RWLIST_WRLOCK(&be_list);
-       AST_RWLIST_TRAVERSE(&be_list, i, list) {
-               if (!strcasecmp(name, i->name)) {
-                       ast_log(LOG_WARNING, "Already have a CDR backend called '%s'\n", name);
-                       AST_RWLIST_UNLOCK(&be_list);
-                       return -1;
-               }
-       }
+static int parked_state_process_bridge_leave(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel);
 
-       if (!(i = ast_calloc(1, sizeof(*i))))
-               return -1;
+/*!
+ * \brief The virtual table for the Parked state
+ *
+ * Parking is weird. Unlike typical bridges, it has to be treated somewhat
+ * uniquely - a channel in a parking bridge (which is a subclass of a holding
+ * bridge) has to be handled as if the channel went into an application.
+ * However, when the channel comes out, we need a new CDR - unlike the Single
+ * state.
+ */
+struct cdr_object_fn_table parked_state_fn_table = {
+       .name = "Parked",
+       .process_party_a = base_process_party_a,
+       .process_bridge_leave = parked_state_process_bridge_leave,
+       .process_parked_channel = base_process_parked_channel,
+};
 
-       i->be = be;
-       ast_copy_string(i->name, name, sizeof(i->name));
-       ast_copy_string(i->desc, desc, sizeof(i->desc));
+static void finalized_state_init_function(struct cdr_object *cdr);
+static int finalized_state_process_party_a(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot);
 
-       AST_RWLIST_INSERT_HEAD(&be_list, i, list);
-       AST_RWLIST_UNLOCK(&be_list);
+/*!
+ * \brief The virtual table for the finalized state.
+ *
+ * Once in the finalized state, the CDR is done. No modifications can be made
+ * to the CDR.
+ */
+struct cdr_object_fn_table finalized_state_fn_table = {
+       .name = "Finalized",
+       .init_function = finalized_state_init_function,
+       .process_party_a = finalized_state_process_party_a,
+       .process_bridge_enter = base_process_bridge_enter,
+};
 
-       return 0;
-}
+/*! \brief A wrapper object around a snapshot.
+ * Fields that are mutable by the CDR engine are replicated here.
+ */
+struct cdr_object_snapshot {
+       struct ast_channel_snapshot *snapshot;  /*!< The channel snapshot */
+       char userfield[AST_MAX_USER_FIELD];     /*!< Userfield for the channel */
+       unsigned int flags;                     /*!< Specific flags for this party */
+       struct varshead variables;              /*!< CDR variables for the channel */
+};
+
+/*! \brief An in-memory representation of an active CDR */
+struct cdr_object {
+       struct cdr_object_snapshot party_a;     /*!< The Party A information */
+       struct cdr_object_snapshot party_b;     /*!< The Party B information */
+       struct cdr_object_fn_table *fn_table;   /*!< The current virtual table */
+
+       enum ast_cdr_disposition disposition;   /*!< The disposition of the CDR */
+       struct timeval start;                   /*!< When this CDR was created */
+       struct timeval answer;                  /*!< Either when the channel was answered, or when the path between channels was established */
+       struct timeval end;                     /*!< When this CDR was finalized */
+       unsigned int sequence;                  /*!< A monotonically increasing number for each CDR */
+       struct ast_flags flags;                 /*!< Flags on the CDR */
+       AST_DECLARE_STRING_FIELDS(
+               AST_STRING_FIELD(linkedid);         /*!< Linked ID. Cached here as it may change out from party A, which must be immutable */
+               AST_STRING_FIELD(uniqueid);                     /*!< Unique id of party A. Cached here as it is the primary key of this CDR */
+               AST_STRING_FIELD(name);             /*!< Channel name of party A. Cached here as the party A address may change */
+               AST_STRING_FIELD(bridge);           /*!< The bridge the party A happens to be in. */
+               AST_STRING_FIELD(appl);             /*!< The last accepted application party A was in */
+               AST_STRING_FIELD(data);             /*!< The data for the last accepted application party A was in */
+               AST_STRING_FIELD(context);          /*!< The accepted context for Party A */
+               AST_STRING_FIELD(exten);            /*!< The accepted extension for Party A */
+       );
+       struct cdr_object *next;                /*!< The next CDR object in the chain */
+       struct cdr_object *last;                /*!< The last CDR object in the chain */
+};
 
-/*! unregister a CDR driver */
-void ast_cdr_unregister(const char *name)
+/*!
+ * \brief Copy variables from one list to another
+ * \param to_list destination
+ * \param from_list source
+ * \retval The number of copied variables
+ */
+static int copy_variables(struct varshead *to_list, struct varshead *from_list)
 {
-       struct ast_cdr_beitem *i = NULL;
+       struct ast_var_t *variables;
+       struct ast_var_t *newvariable;
+       const char *var;
+       const char *val;
+       int x = 0;
 
-       AST_RWLIST_WRLOCK(&be_list);
-       AST_RWLIST_TRAVERSE_SAFE_BEGIN(&be_list, i, list) {
-               if (!strcasecmp(name, i->name)) {
-                       AST_RWLIST_REMOVE_CURRENT(list);
-                       break;
+       AST_LIST_TRAVERSE(from_list, variables, entries) {
+               var = ast_var_name(variables);
+               if (ast_strlen_zero(var)) {
+                       continue;
+               }
+               val = ast_var_value(variables);
+               if (ast_strlen_zero(val)) {
+                       continue;
+               }
+               newvariable = ast_var_assign(var, val);
+               if (newvariable) {
+                       AST_LIST_INSERT_HEAD(to_list, newvariable, entries);
+                       ++x;
                }
        }
-       AST_RWLIST_TRAVERSE_SAFE_END;
-       AST_RWLIST_UNLOCK(&be_list);
 
-       if (i) {
-               ast_verb(2, "Unregistered '%s' CDR backend\n", name);
-               ast_free(i);
+       return x;
+}
+
+/*!
+ * \brief Delete all variables from a variable list
+ * \param headp The head pointer to the variable list to delete
+ */
+static void free_variables(struct varshead *headp)
+{
+       struct ast_var_t *vardata;
+
+       while ((vardata = AST_LIST_REMOVE_HEAD(headp, entries))) {
+               ast_var_delete(vardata);
        }
 }
 
-int ast_cdr_isset_unanswered(void)
+/*!
+ * \brief Copy a snapshot and its details
+ * \param dst The destination
+ * \param src The source
+ */
+static void cdr_object_snapshot_copy(struct cdr_object_snapshot *dst, struct cdr_object_snapshot *src)
 {
-       return unanswered;
+       if (dst->snapshot) {
+               ao2_t_ref(dst->snapshot, -1, "release old snapshot during copy");
+       }
+       dst->snapshot = src->snapshot;
+       ao2_t_ref(dst->snapshot, +1, "bump new snapshot during copy");
+       strcpy(dst->userfield, src->userfield);
+       dst->flags = src->flags;
+       copy_variables(&dst->variables, &src->variables);
 }
 
-struct ast_cdr *ast_cdr_dup_unique(struct ast_cdr *cdr)
+/*!
+ * \brief Transition a \ref cdr_object to a new state
+ * \param cdr The \ref cdr_object to transition
+ * \param fn_table The \ref cdr_object_fn_table state to go to
+ */
+static void cdr_object_transition_state(struct cdr_object *cdr, struct cdr_object_fn_table *fn_table)
 {
-       struct ast_cdr *newcdr = ast_cdr_dup(cdr);
-       if (!newcdr)
-               return NULL;
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       CDR_DEBUG(mod_cfg, "%p - Transitioning CDR for %s from state %s to %s\n",
+               cdr, cdr->party_a.snapshot->name,
+               cdr->fn_table ? cdr->fn_table->name : "NONE", fn_table->name);
+       cdr->fn_table = fn_table;
+       if (cdr->fn_table->init_function) {
+               cdr->fn_table->init_function(cdr);
+       }
+}
+/*! \internal
+ * \brief Hash function for containers of CDRs indexing by Party A uniqueid */
+static int cdr_object_channel_hash_fn(const void *obj, const int flags)
+{
+       const struct cdr_object *cdr;
+       const char *key;
+
+       switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) {
+       case OBJ_KEY:
+               key = obj;
+               break;
+       case OBJ_POINTER:
+               cdr = obj;
+               key = cdr->uniqueid;
+               break;
+       default:
+               ast_assert(0);
+               return 0;
+       }
+       return ast_str_case_hash(key);
+}
 
-       cdr_seq_inc(newcdr);
-       return newcdr;
+/*! \internal
+ * \brief Comparison function for containers of CDRs indexing by Party A uniqueid
+ */
+static int cdr_object_channel_cmp_fn(void *obj, void *arg, int flags)
+{
+    struct cdr_object *left = obj;
+    struct cdr_object *right = arg;
+    const char *right_key = arg;
+    int cmp;
+
+    switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) {
+    case OBJ_POINTER:
+        right_key = right->uniqueid;
+        /* Fall through */
+    case OBJ_KEY:
+        cmp = strcmp(left->uniqueid, right_key);
+        break;
+    case OBJ_PARTIAL_KEY:
+        /*
+         * We could also use a partial key struct containing a length
+         * so strlen() does not get called for every comparison instead.
+         */
+        cmp = strncmp(left->uniqueid, right_key, strlen(right_key));
+        break;
+    default:
+        /* Sort can only work on something with a full or partial key. */
+        ast_assert(0);
+        cmp = 0;
+        break;
+    }
+    return cmp ? 0 : CMP_MATCH;
 }
 
-struct ast_cdr *ast_cdr_dup_unique_swap(struct ast_cdr *cdr)
+/*!
+ * \brief \ref cdr_object Destructor
+ */
+static void cdr_object_dtor(void *obj)
 {
-       struct ast_cdr *newcdr = ast_cdr_dup(cdr);
-       if (!newcdr)
-               return NULL;
+       struct cdr_object *cdr = obj;
+       struct ast_var_t *it_var;
 
-       cdr_seq_inc(cdr);
-       return newcdr;
+       ao2_cleanup(cdr->party_a.snapshot);
+       ao2_cleanup(cdr->party_b.snapshot);
+       while ((it_var = AST_LIST_REMOVE_HEAD(&cdr->party_a.variables, entries))) {
+               ast_var_delete(it_var);
+       }
+       while ((it_var = AST_LIST_REMOVE_HEAD(&cdr->party_b.variables, entries))) {
+               ast_var_delete(it_var);
+       }
+       ast_string_field_free_memory(cdr);
+
+       ao2_cleanup(cdr->next);
 }
 
-/*! Duplicate a CDR record
-       \returns Pointer to new CDR record
-*/
-struct ast_cdr *ast_cdr_dup(struct ast_cdr *cdr)
+/*!
+ * \brief \ref cdr_object constructor
+ * \param chan The \ref ast_channel_snapshot that is the CDR's Party A
+ *
+ * This implicitly sets the state of the newly created CDR to the Single state
+ * (\ref single_state_fn_table)
+ */
+static struct cdr_object *cdr_object_alloc(struct ast_channel_snapshot *chan)
 {
-       struct ast_cdr *newcdr;
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       struct cdr_object *cdr;
+
+       ast_assert(chan != NULL);
 
-       if (!cdr) /* don't die if we get a null cdr pointer */
+       cdr = ao2_alloc(sizeof(*cdr), cdr_object_dtor);
+       if (!cdr) {
                return NULL;
-       newcdr = ast_cdr_alloc();
-       if (!newcdr)
+       }
+       cdr->last = cdr;
+       if (ast_string_field_init(cdr, 64)) {
+               ao2_cleanup(cdr);
                return NULL;
+       }
+       ast_string_field_set(cdr, uniqueid, chan->uniqueid);
+       ast_string_field_set(cdr, name, chan->name);
+       ast_string_field_set(cdr, linkedid, chan->linkedid);
+       cdr->disposition = AST_CDR_NULL;
+       cdr->sequence = ast_atomic_fetchadd_int(&global_cdr_sequence, +1);
 
-       memcpy(newcdr, cdr, sizeof(*newcdr));
-       /* The varshead is unusable, volatile even, after the memcpy so we take care of that here */
-       memset(&newcdr->varshead, 0, sizeof(newcdr->varshead));
-       ast_cdr_copy_vars(newcdr, cdr);
-       newcdr->next = NULL;
+       cdr->party_a.snapshot = chan;
+       ao2_t_ref(cdr->party_a.snapshot, +1, "bump snapshot during CDR creation");
 
-       return newcdr;
+       CDR_DEBUG(mod_cfg, "%p - Created CDR for channel %s\n", cdr, chan->name);
+
+       cdr_object_transition_state(cdr, &single_state_fn_table);
+
+       return cdr;
 }
 
-static const char *ast_cdr_getvar_internal(struct ast_cdr *cdr, const char *name, int recur)
+/*!
+ * \brief Create a new \ref cdr_object and append it to an existing chain
+ * \param cdr The \ref cdr_object to append to
+ */
+static struct cdr_object *cdr_object_create_and_append(struct cdr_object *cdr)
 {
-       if (ast_strlen_zero(name))
-               return NULL;
+       struct cdr_object *new_cdr;
+       struct cdr_object *it_cdr;
+       struct cdr_object *cdr_last;
 
-       for (; cdr; cdr = recur ? cdr->next : NULL) {
-               struct ast_var_t *variables;
-               struct varshead *headp = &cdr->varshead;
-               AST_LIST_TRAVERSE(headp, variables, entries) {
-                       if (!strcasecmp(name, ast_var_name(variables)))
-                               return ast_var_value(variables);
-               }
+       cdr_last = cdr->last;
+       new_cdr = cdr_object_alloc(cdr_last->party_a.snapshot);
+       if (!new_cdr) {
+               return NULL;
        }
+       new_cdr->disposition = AST_CDR_NULL;
 
-       return NULL;
-}
+       /* Copy over the linkedid, as it may have changed */
+       ast_string_field_set(new_cdr, linkedid, cdr_last->linkedid);
+       ast_string_field_set(new_cdr, appl, cdr_last->appl);
+       ast_string_field_set(new_cdr, data, cdr_last->data);
 
-static void cdr_get_tv(struct timeval when, const char *fmt, char *buf, int bufsize)
-{
-       if (fmt == NULL) {      /* raw mode */
-               snprintf(buf, bufsize, "%ld.%06ld", (long)when.tv_sec, (long)when.tv_usec);
-       } else {
-               if (when.tv_sec) {
-                       struct ast_tm tm;
+       /* Copy over other Party A information */
+       cdr_object_snapshot_copy(&new_cdr->party_a, &cdr_last->party_a);
 
-                       ast_localtime(&when, &tm, NULL);
-                       ast_strftime(buf, bufsize, fmt, &tm);
-               }
+       /* Append the CDR to the end of the list */
+       for (it_cdr = cdr; it_cdr->next; it_cdr = it_cdr->next) {
+               it_cdr->last = new_cdr;
        }
+       it_cdr->last = new_cdr;
+       it_cdr->next = new_cdr;
+
+       return new_cdr;
 }
 
-/*! CDR channel variable retrieval */
-void ast_cdr_getvar(struct ast_cdr *cdr, const char *name, char **ret, char *workspace, int workspacelen, int recur, int raw)
+/*!
+ * \brief Return whether or not a channel has changed its state in the dialplan, subject
+ * to endbeforehexten logic
+ *
+ * \param old_snapshot The previous state
+ * \param new_snapshot The new state
+ *
+ * \retval 0 if the state has not changed
+ * \retval 1 if the state changed
+ */
+static int snapshot_cep_changed(struct ast_channel_snapshot *old_snapshot,
+       struct ast_channel_snapshot *new_snapshot)
 {
-       const char *fmt = "%Y-%m-%d %T";
-       const char *varbuf;
-
-       if (!cdr)  /* don't die if the cdr is null */
-               return;
+       RAII_VAR(struct module_config *, mod_cfg,
+               ao2_global_obj_ref(module_configs), ao2_cleanup);
 
-       *ret = NULL;
-       /* special vars (the ones from the struct ast_cdr when requested by name)
-          I'd almost say we should convert all the stringed vals to vars */
+       /* If we ignore hangup logic, don't indicate that we're executing anything new */
+       if (ast_test_flag(&mod_cfg->general->settings, CDR_END_BEFORE_H_EXTEN)
+               && ast_test_flag(&new_snapshot->softhangup_flags, AST_SOFTHANGUP_HANGUP_EXEC)) {
+               return 0;
+       }
 
-       if (!strcasecmp(name, "clid"))
-               ast_copy_string(workspace, cdr->clid, workspacelen);
-       else if (!strcasecmp(name, "src"))
-               ast_copy_string(workspace, cdr->src, workspacelen);
-       else if (!strcasecmp(name, "dst"))
-               ast_copy_string(workspace, cdr->dst, workspacelen);
-       else if (!strcasecmp(name, "dcontext"))
-               ast_copy_string(workspace, cdr->dcontext, workspacelen);
-       else if (!strcasecmp(name, "channel"))
-               ast_copy_string(workspace, cdr->channel, workspacelen);
-       else if (!strcasecmp(name, "dstchannel"))
-               ast_copy_string(workspace, cdr->dstchannel, workspacelen);
-       else if (!strcasecmp(name, "lastapp"))
-               ast_copy_string(workspace, cdr->lastapp, workspacelen);
-       else if (!strcasecmp(name, "lastdata"))
-               ast_copy_string(workspace, cdr->lastdata, workspacelen);
-       else if (!strcasecmp(name, "start"))
-               cdr_get_tv(cdr->start, raw ? NULL : fmt, workspace, workspacelen);
-       else if (!strcasecmp(name, "answer"))
-               cdr_get_tv(cdr->answer, raw ? NULL : fmt, workspace, workspacelen);
-       else if (!strcasecmp(name, "end"))
-               cdr_get_tv(cdr->end, raw ? NULL : fmt, workspace, workspacelen);
-       else if (!strcasecmp(name, "duration"))
-               snprintf(workspace, workspacelen, "%ld", cdr->duration ? cdr->duration : (long)ast_tvdiff_ms(ast_tvnow(), cdr->start) / 1000);
-       else if (!strcasecmp(name, "billsec"))
-               snprintf(workspace, workspacelen, "%ld", cdr->billsec || cdr->answer.tv_sec == 0 ? cdr->billsec : (long)ast_tvdiff_ms(ast_tvnow(), cdr->answer) / 1000);
-       else if (!strcasecmp(name, "disposition")) {
-               if (raw) {
-                       snprintf(workspace, workspacelen, "%ld", cdr->disposition);
-               } else {
-                       ast_copy_string(workspace, ast_cdr_disp2str(cdr->disposition), workspacelen);
-               }
-       } else if (!strcasecmp(name, "amaflags")) {
-               if (raw) {
-                       snprintf(workspace, workspacelen, "%ld", cdr->amaflags);
-               } else {
-                       ast_copy_string(workspace, ast_cdr_flags2str(cdr->amaflags), workspacelen);
-               }
-       } else if (!strcasecmp(name, "accountcode"))
-               ast_copy_string(workspace, cdr->accountcode, workspacelen);
-       else if (!strcasecmp(name, "peeraccount"))
-               ast_copy_string(workspace, cdr->peeraccount, workspacelen);
-       else if (!strcasecmp(name, "uniqueid"))
-               ast_copy_string(workspace, cdr->uniqueid, workspacelen);
-       else if (!strcasecmp(name, "linkedid"))
-               ast_copy_string(workspace, cdr->linkedid, workspacelen);
-       else if (!strcasecmp(name, "userfield"))
-               ast_copy_string(workspace, cdr->userfield, workspacelen);
-       else if (!strcasecmp(name, "sequence"))
-               snprintf(workspace, workspacelen, "%d", cdr->sequence);
-       else if ((varbuf = ast_cdr_getvar_internal(cdr, name, recur)))
-               ast_copy_string(workspace, varbuf, workspacelen);
-       else
-               workspace[0] = '\0';
+       /* When Party A is originated to an application and the application exits, the stack
+        * will attempt to clear the application and restore the dummy originate application
+        * of "AppDialX". Ignore application changes to AppDialX as a result.
+        */
+       if (strcmp(new_snapshot->appl, old_snapshot->appl) && strncasecmp(new_snapshot->appl, "appdial", 7)
+               && (strcmp(new_snapshot->context, old_snapshot->context)
+               || strcmp(new_snapshot->exten, old_snapshot->exten)
+               || new_snapshot->priority != old_snapshot->priority)) {
+               return 1;
+       }
 
-       if (!ast_strlen_zero(workspace))
-               *ret = workspace;
+       return 0;
 }
 
-/* readonly cdr variables */
-static const char * const cdr_readonly_vars[] = { "clid", "src", "dst", "dcontext", "channel", "dstchannel",
-                                                 "lastapp", "lastdata", "start", "answer", "end", "duration",
-                                                 "billsec", "disposition", "amaflags", "accountcode", "uniqueid", "linkedid",
-                                                 "userfield", "sequence", NULL };
-/*! Set a CDR channel variable
-       \note You can't set the CDR variables that belong to the actual CDR record, like "billsec".
-*/
-int ast_cdr_setvar(struct ast_cdr *cdr, const char *name, const char *value, int recur)
+/*!
+ * \brief Return whether or not a \ref ast_channel_snapshot is for a channel
+ * that was created as the result of a dial operation
+ *
+ * \retval 0 the channel was not created as the result of a dial
+ * \retval 1 the channel was created as the result of a dial
+ */
+static int snapshot_is_dialed(struct ast_channel_snapshot *snapshot)
 {
-       struct ast_var_t *newvariable;
-       struct varshead *headp;
-       int x;
+       return (ast_test_flag(&snapshot->flags, AST_FLAG_OUTGOING)
+                       && !(ast_test_flag(&snapshot->flags, AST_FLAG_ORIGINATED)));
+}
 
-       for (x = 0; cdr_readonly_vars[x]; x++) {
-               if (!strcasecmp(name, cdr_readonly_vars[x])) {
-                       ast_log(LOG_ERROR, "Attempt to set the '%s' read-only variable!.\n", name);
-                       return -1;
-               }
+/*!
+ * \brief Given two CDR snapshots, figure out who should be Party A for the
+ * resulting CDR
+ * \param left One of the snapshots
+ * \param right The other snapshot
+ * \retval The snapshot that won
+ */
+static struct cdr_object_snapshot *cdr_object_pick_party_a(struct cdr_object_snapshot *left, struct cdr_object_snapshot *right)
+{
+       /* Check whether or not the party is dialed. A dialed party is never the
+        * Party A with a party that was not dialed.
+        */
+       if (!snapshot_is_dialed(left->snapshot) && snapshot_is_dialed(right->snapshot)) {
+               return left;
+       } else if (snapshot_is_dialed(left->snapshot) && !snapshot_is_dialed(right->snapshot)) {
+               return right;
        }
 
-       if (!cdr) {
-               ast_log(LOG_ERROR, "Attempt to set a variable on a nonexistent CDR record.\n");
-               return -1;
+       /* Try the Party A flag */
+       if (ast_test_flag(left, AST_CDR_FLAG_PARTY_A) && !ast_test_flag(right, AST_CDR_FLAG_PARTY_A)) {
+               return left;
+       } else if (!ast_test_flag(right, AST_CDR_FLAG_PARTY_A) && ast_test_flag(right, AST_CDR_FLAG_PARTY_A)) {
+               return right;
        }
 
-       for (; cdr; cdr = recur ? cdr->next : NULL) {
-               if (ast_test_flag(cdr, AST_CDR_FLAG_DONT_TOUCH) && ast_test_flag(cdr, AST_CDR_FLAG_LOCKED))
-                       continue;
-               headp = &cdr->varshead;
-               AST_LIST_TRAVERSE_SAFE_BEGIN(headp, newvariable, entries) {
-                       if (!strcasecmp(ast_var_name(newvariable), name)) {
-                               /* there is already such a variable, delete it */
-                               AST_LIST_REMOVE_CURRENT(entries);
-                               ast_var_delete(newvariable);
-                               break;
-                       }
-               }
-               AST_LIST_TRAVERSE_SAFE_END;
-
-               if (value) {
-                       newvariable = ast_var_assign(name, value);
-                       AST_LIST_INSERT_HEAD(headp, newvariable, entries);
-               }
+       /* Neither party is dialed and neither has the Party A flag - defer to
+        * creation time */
+       if (left->snapshot->creationtime.tv_sec < right->snapshot->creationtime.tv_sec) {
+               return left;
+       } else if (left->snapshot->creationtime.tv_sec > right->snapshot->creationtime.tv_sec) {
+               return right;
+       } else if (left->snapshot->creationtime.tv_usec > right->snapshot->creationtime.tv_usec) {
+               return right;
+       } else {
+               /* Okay, fine, take the left one */
+               return left;
        }
+}
 
-       return 0;
+/*!
+ * Compute the duration for a \ref cdr_object
+ */
+static long cdr_object_get_duration(struct cdr_object *cdr)
+{
+       return (long)(ast_tvdiff_ms(ast_tvzero(cdr->end) ? ast_tvnow() : cdr->end, cdr->start) / 1000);
 }
 
-int ast_cdr_copy_vars(struct ast_cdr *to_cdr, struct ast_cdr *from_cdr)
+/*!
+ * \brief Compute the billsec for a \ref cdr_object
+ */
+static long cdr_object_get_billsec(struct cdr_object *cdr)
 {
-       struct ast_var_t *variables, *newvariable = NULL;
-       struct varshead *headpa, *headpb;
-       const char *var, *val;
-       int x = 0;
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       long int ms;
 
-       if (!to_cdr || !from_cdr) /* don't die if one of the pointers is null */
+       if (ast_tvzero(cdr->answer)) {
                return 0;
+       }
+       ms = ast_tvdiff_ms(ast_tvzero(cdr->end) ? ast_tvnow() : cdr->end, cdr->answer);
+       if (ast_test_flag(&mod_cfg->general->settings, CDR_INITIATED_SECONDS)
+               && (ms % 1000 >= 500)) {
+               ms = (ms / 1000) + 1;
+       } else {
+               ms = ms / 1000;
+       }
+
+       return ms;
+}
 
-       headpa = &from_cdr->varshead;
-       headpb = &to_cdr->varshead;
+/*!
+ * \internal
+ * \brief Set a variable on a CDR object
+ *
+ * \param headp The header pointer to the variable to set
+ * \param name The name of the variable
+ * \param value The value of the variable
+ */
+static void set_variable(struct varshead *headp, const char *name, const char *value)
+{
+       struct ast_var_t *newvariable;
 
-       AST_LIST_TRAVERSE(headpa,variables,entries) {
-               if (variables &&
-                   (var = ast_var_name(variables)) && (val = ast_var_value(variables)) &&
-                   !ast_strlen_zero(var) && !ast_strlen_zero(val)) {
-                       newvariable = ast_var_assign(var, val);
-                       AST_LIST_INSERT_HEAD(headpb, newvariable, entries);
-                       x++;
+       AST_LIST_TRAVERSE_SAFE_BEGIN(headp, newvariable, entries) {
+               if (!strcasecmp(ast_var_name(newvariable), name)) {
+                       AST_LIST_REMOVE_CURRENT(entries);
+                       ast_var_delete(newvariable);
+                       break;
                }
        }
+       AST_LIST_TRAVERSE_SAFE_END;
 
-       return x;
+       if (value && (newvariable = ast_var_assign(name, value))) {
+               AST_LIST_INSERT_HEAD(headp, newvariable, entries);
+       }
 }
 
-int ast_cdr_serialize_variables(struct ast_cdr *cdr, struct ast_str **buf, char delim, char sep, int recur)
+/*!
+ * \brief Create a chain of \ref ast_cdr objects from a chain of \ref cdr_object
+ * suitable for consumption by the registered CDR backends
+ * \param cdr The \ref cdr_object to convert to a public record
+ * \retval A chain of \ref ast_cdr objects on success
+ * \retval NULL on failure
+ */
+static struct ast_cdr *cdr_object_create_public_records(struct cdr_object *cdr)
 {
-       struct ast_var_t *variables;
-       const char *var;
-       char *tmp;
-       char workspace[256];
-       int total = 0, x = 0, i;
-
-       ast_str_reset(*buf);
+       struct ast_cdr *pub_cdr = NULL, *cdr_prev = NULL;
+       struct cdr_object *it_cdr;
+       struct ast_var_t *it_var, *it_copy_var;
+       struct ast_channel_snapshot *party_a;
+       struct ast_channel_snapshot *party_b;
+
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               struct ast_cdr *cdr_copy;
+
+               /* Don't create records for CDRs where the party A was a dialed channel */
+               if (snapshot_is_dialed(it_cdr->party_a.snapshot) && !it_cdr->party_b.snapshot) {
+                       ast_debug(1, "CDR for %s is dialed and has no Party B; discarding\n",
+                               it_cdr->party_a.snapshot->name);
+                       continue;
+               }
 
-       for (; cdr; cdr = recur ? cdr->next : NULL) {
-               if (++x > 1)
-                       ast_str_append(buf, 0, "\n");
+               cdr_copy = ast_calloc(1, sizeof(*cdr_copy));
+               if (!cdr_copy) {
+                       ast_free(pub_cdr);
+                       return NULL;
+               }
 
-               AST_LIST_TRAVERSE(&cdr->varshead, variables, entries) {
-                       if (!(var = ast_var_name(variables))) {
-                               continue;
+               party_a = it_cdr->party_a.snapshot;
+               party_b = it_cdr->party_b.snapshot;
+
+               /* Party A */
+               ast_assert(party_a != NULL);
+               ast_copy_string(cdr_copy->accountcode, party_a->accountcode, sizeof(cdr_copy->accountcode));
+               cdr_copy->amaflags = party_a->amaflags;
+               ast_copy_string(cdr_copy->channel, party_a->name, sizeof(cdr_copy->channel));
+               ast_callerid_merge(cdr_copy->clid, sizeof(cdr_copy->clid), party_a->caller_name, party_a->caller_number, "");
+               ast_copy_string(cdr_copy->src, party_a->caller_number, sizeof(cdr_copy->src));
+               ast_copy_string(cdr_copy->uniqueid, party_a->uniqueid, sizeof(cdr_copy->uniqueid));
+               ast_copy_string(cdr_copy->lastapp, it_cdr->appl, sizeof(cdr_copy->lastapp));
+               ast_copy_string(cdr_copy->lastdata, it_cdr->data, sizeof(cdr_copy->lastdata));
+               ast_copy_string(cdr_copy->dst, it_cdr->exten, sizeof(cdr_copy->dst));
+               ast_copy_string(cdr_copy->dcontext, it_cdr->context, sizeof(cdr_copy->dcontext));
+
+               /* Party B */
+               if (party_b) {
+                       ast_copy_string(cdr_copy->dstchannel, party_b->name, sizeof(cdr_copy->dstchannel));
+                       ast_copy_string(cdr_copy->peeraccount, party_b->accountcode, sizeof(cdr_copy->peeraccount));
+                       if (!ast_strlen_zero(it_cdr->party_b.userfield)) {
+                               snprintf(cdr_copy->userfield, sizeof(cdr_copy->userfield), "%s;%s", it_cdr->party_a.userfield, it_cdr->party_b.userfield);
                        }
-
-                       if (ast_str_append(buf, 0, "level %d: %s%c%s%c", x, var, delim, S_OR(ast_var_value(variables), ""), sep) < 0) {
-                               ast_log(LOG_ERROR, "Data Buffer Size Exceeded!\n");
+               }
+               if (ast_strlen_zero(cdr_copy->userfield) && !ast_strlen_zero(it_cdr->party_a.userfield)) {
+                       ast_copy_string(cdr_copy->userfield, it_cdr->party_a.userfield, sizeof(cdr_copy->userfield));
+               }
+
+               /* Timestamps/durations */
+               cdr_copy->start = it_cdr->start;
+               cdr_copy->answer = it_cdr->answer;
+               cdr_copy->end = it_cdr->end;
+               cdr_copy->billsec = cdr_object_get_billsec(it_cdr);
+               cdr_copy->duration = cdr_object_get_duration(it_cdr);
+
+               /* Flags and IDs */
+               ast_copy_flags(cdr_copy, &it_cdr->flags, AST_FLAGS_ALL);
+               ast_copy_string(cdr_copy->linkedid, it_cdr->linkedid, sizeof(cdr_copy->linkedid));
+               cdr_copy->disposition = it_cdr->disposition;
+               cdr_copy->sequence = it_cdr->sequence;
+
+               /* Variables */
+               copy_variables(&cdr_copy->varshead, &it_cdr->party_a.variables);
+               AST_LIST_TRAVERSE(&it_cdr->party_b.variables, it_var, entries) {
+                       int found = 0;
+                       struct ast_var_t *newvariable;
+                       AST_LIST_TRAVERSE(&cdr_copy->varshead, it_copy_var, entries) {
+                               if (!strcasecmp(ast_var_name(it_var), ast_var_name(it_copy_var))) {
+                                       found = 1;
+                                       break;
+                               }
+                       }
+                       if (!found && (newvariable = ast_var_assign(ast_var_name(it_var), ast_var_value(it_var)))) {
+                               AST_LIST_INSERT_TAIL(&cdr_copy->varshead, newvariable, entries);
+                       }
+               }
+
+               if (!pub_cdr) {
+                       pub_cdr = cdr_copy;
+                       cdr_prev = pub_cdr;
+               } else {
+                       cdr_prev->next = cdr_copy;
+                       cdr_prev = cdr_copy;
+               }
+       }
+
+       return pub_cdr;
+}
+
+/*!
+ * \brief Dispatch a CDR.
+ * \param cdr The \ref cdr_object to dispatch
+ *
+ * This will create a \ref ast_cdr object and publish it to the various backends
+ */
+static void cdr_object_dispatch(struct cdr_object *cdr)
+{
+       RAII_VAR(struct module_config *, mod_cfg,
+                       ao2_global_obj_ref(module_configs), ao2_cleanup);
+       struct ast_cdr *pub_cdr;
+
+       CDR_DEBUG(mod_cfg, "%p - Dispatching CDR for Party A %s, Party B %s\n", cdr,
+                       cdr->party_a.snapshot->name,
+                       cdr->party_b.snapshot ? cdr->party_b.snapshot->name : "<none>");
+       pub_cdr = cdr_object_create_public_records(cdr);
+       cdr_detach(pub_cdr);
+}
+
+/*!
+ * \brief Set the disposition on a \ref cdr_object based on a hangupcause code
+ * \param cdr The \ref cdr_object
+ * \param hangupcause The Asterisk hangup cause code
+ */
+static void cdr_object_set_disposition(struct cdr_object *cdr, int hangupcause)
+{
+       RAII_VAR(struct module_config *, mod_cfg,
+                       ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       /* Change the disposition based on the hang up cause */
+       switch (hangupcause) {
+       case AST_CAUSE_BUSY:
+               cdr->disposition = AST_CDR_BUSY;
+               break;
+       case AST_CAUSE_CONGESTION:
+               if (!ast_test_flag(&mod_cfg->general->settings, CDR_CONGESTION)) {
+                       cdr->disposition = AST_CDR_FAILED;
+               } else {
+                       cdr->disposition = AST_CDR_CONGESTION;
+               }
+               break;
+       case AST_CAUSE_NO_ROUTE_DESTINATION:
+       case AST_CAUSE_UNREGISTERED:
+               cdr->disposition = AST_CDR_FAILED;
+               break;
+       case AST_CAUSE_NORMAL_CLEARING:
+       case AST_CAUSE_NO_ANSWER:
+               cdr->disposition = AST_CDR_NOANSWER;
+               break;
+       default:
+               break;
+       }
+}
+
+/*!
+ * \brief Finalize a CDR.
+ *
+ * This function is safe to call multiple times. Note that you can call this
+ * explicitly before going to the finalized state if there's a chance the CDR
+ * will be re-activated, in which case the \ref cdr_object's end time should be
+ * cleared. This function is implicitly called when a CDR transitions to the
+ * finalized state and right before it is dispatched
+ *
+ * \param cdr_object The CDR to finalize
+ */
+static void cdr_object_finalize(struct cdr_object *cdr)
+{
+       if (!ast_tvzero(cdr->end)) {
+               return;
+       }
+       cdr->end = ast_tvnow();
+
+       if (cdr->disposition == AST_CDR_NULL) {
+               if (!ast_tvzero(cdr->answer)) {
+                       cdr->disposition = AST_CDR_ANSWERED;
+               } else if (cdr->party_a.snapshot->hangupcause) {
+                       cdr_object_set_disposition(cdr, cdr->party_a.snapshot->hangupcause);
+               } else if (cdr->party_b.snapshot && cdr->party_b.snapshot->hangupcause) {
+                       cdr_object_set_disposition(cdr, cdr->party_b.snapshot->hangupcause);
+               } else {
+                       cdr->disposition = AST_CDR_FAILED;
+               }
+       }
+
+       /* tv_usec is suseconds_t, which could be int or long */
+       ast_debug(1, "Finalized CDR for %s - start %ld.%06ld answer %ld.%06ld end %ld.%06ld dispo %s\n",
+                       cdr->party_a.snapshot->name,
+                       (long)cdr->start.tv_sec,
+                       (long)cdr->start.tv_usec,
+                       (long)cdr->answer.tv_sec,
+                       (long)cdr->answer.tv_usec,
+                       (long)cdr->end.tv_sec,
+                       (long)cdr->end.tv_usec,
+                       ast_cdr_disp2str(cdr->disposition));
+}
+
+/*!
+ * \brief Check to see if a CDR needs to move to the finalized state because
+ * its Party A hungup.
+ */
+static void cdr_object_check_party_a_hangup(struct cdr_object *cdr)
+{
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       if (ast_test_flag(&mod_cfg->general->settings, CDR_END_BEFORE_H_EXTEN)
+               && ast_test_flag(&cdr->party_a.snapshot->softhangup_flags, AST_SOFTHANGUP_HANGUP_EXEC)) {
+               cdr_object_finalize(cdr);
+       }
+
+       if (ast_test_flag(&cdr->party_a.snapshot->flags, AST_FLAG_DEAD)
+               && cdr->fn_table != &finalized_state_fn_table) {
+               cdr_object_transition_state(cdr, &finalized_state_fn_table);
+       }
+}
+
+/*!
+ * \brief Check to see if a CDR needs to be answered based on its Party A.
+ * Note that this is safe to call as much as you want - we won't answer twice
+ */
+static void cdr_object_check_party_a_answer(struct cdr_object *cdr) {
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       if (cdr->party_a.snapshot->state == AST_STATE_UP && ast_tvzero(cdr->answer)) {
+               cdr->answer = ast_tvnow();
+               /* tv_usec is suseconds_t, which could be int or long */
+               CDR_DEBUG(mod_cfg, "%p - Set answered time to %ld.%06ld\n", cdr,
+                       (long)cdr->answer.tv_sec,
+                       (long)cdr->answer.tv_usec);
+       }
+}
+
+/* \brief Set Caller ID information on a CDR */
+static void cdr_object_update_cid(struct cdr_object_snapshot *old_snapshot, struct ast_channel_snapshot *new_snapshot)
+{
+       if (!old_snapshot->snapshot) {
+               set_variable(&old_snapshot->variables, "dnid", new_snapshot->caller_dnid);
+               set_variable(&old_snapshot->variables, "callingsubaddr", new_snapshot->caller_subaddr);
+               set_variable(&old_snapshot->variables, "calledsubaddr", new_snapshot->dialed_subaddr);
+               return;
+       }
+       if (!strcmp(old_snapshot->snapshot->caller_dnid, new_snapshot->caller_dnid)) {
+               set_variable(&old_snapshot->variables, "dnid", new_snapshot->caller_dnid);
+       }
+       if (!strcmp(old_snapshot->snapshot->caller_subaddr, new_snapshot->caller_subaddr)) {
+               set_variable(&old_snapshot->variables, "callingsubaddr", new_snapshot->caller_subaddr);
+       }
+       if (!strcmp(old_snapshot->snapshot->dialed_subaddr, new_snapshot->dialed_subaddr)) {
+               set_variable(&old_snapshot->variables, "calledsubaddr", new_snapshot->dialed_subaddr);
+       }
+}
+
+/*!
+ * \brief Swap an old \ref cdr_object_snapshot's \ref ast_channel_snapshot for
+ * a new \ref ast_channel_snapshot
+ * \param old_snapshot The old \ref cdr_object_snapshot
+ * \param new_snapshot The new \ref ast_channel_snapshot for old_snapshot
+ */
+static void cdr_object_swap_snapshot(struct cdr_object_snapshot *old_snapshot,
+               struct ast_channel_snapshot *new_snapshot)
+{
+       cdr_object_update_cid(old_snapshot, new_snapshot);
+       if (old_snapshot->snapshot) {
+               ao2_t_ref(old_snapshot->snapshot, -1, "Drop ref for swap");
+       }
+       ao2_t_ref(new_snapshot, +1, "Bump ref for swap");
+       old_snapshot->snapshot = new_snapshot;
+}
+
+/* BASE METHOD IMPLEMENTATIONS */
+
+static int base_process_party_a(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot)
+{
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       ast_assert(strcasecmp(snapshot->name, cdr->party_a.snapshot->name) == 0);
+
+       /* Ignore any snapshots from a dead or dying channel */
+       if (ast_test_flag(&snapshot->softhangup_flags, AST_SOFTHANGUP_HANGUP_EXEC)
+                       && ast_test_flag(&mod_cfg->general->settings, CDR_END_BEFORE_H_EXTEN)) {
+               cdr_object_check_party_a_hangup(cdr);
+               return 0;
+       }
+
+       /*
+        * Only record the context and extension if we aren't in a subroutine, or if
+        * we are executing hangup logic.
+        */
+       if (!ast_test_flag(&snapshot->flags, AST_FLAG_SUBROUTINE_EXEC)
+               || ast_test_flag(&snapshot->softhangup_flags, AST_SOFTHANGUP_HANGUP_EXEC)) {
+               ast_string_field_set(cdr, context, snapshot->context);
+               ast_string_field_set(cdr, exten, snapshot->exten);
+       }
+
+       cdr_object_swap_snapshot(&cdr->party_a, snapshot);
+
+       /* When Party A is originated to an application and the application exits, the stack
+        * will attempt to clear the application and restore the dummy originate application
+        * of "AppDialX". Prevent that, and any other application changes we might not want
+        * here.
+        */
+       if (!ast_strlen_zero(snapshot->appl)
+                       && (strncasecmp(snapshot->appl, "appdial", 7) || ast_strlen_zero(cdr->appl))
+                       && !ast_test_flag(&cdr->flags, AST_CDR_LOCK_APP)) {
+               ast_string_field_set(cdr, appl, snapshot->appl);
+               ast_string_field_set(cdr, data, snapshot->data);
+
+               /* Dial (app_dial) is a special case. Because pre-dial handlers, which
+                * execute before the dial begins, will alter the application/data to
+                * something people typically don't want to see, if we see a channel enter
+                * into Dial here, we set the appl/data accordingly and lock it.
+                */
+               if (!strcmp(snapshot->appl, "Dial")) {
+                       ast_set_flag(&cdr->flags, AST_CDR_LOCK_APP);
+               }
+       }
+
+       ast_string_field_set(cdr, linkedid, snapshot->linkedid);
+       cdr_object_check_party_a_answer(cdr);
+       cdr_object_check_party_a_hangup(cdr);
+
+       return 0;
+}
+
+static int base_process_bridge_leave(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel)
+{
+       /* In general, most things shouldn't get a bridge leave */
+       ast_assert(0);
+       return 1;
+}
+
+static int base_process_dial_end(struct cdr_object *cdr, struct ast_channel_snapshot *caller, struct ast_channel_snapshot *peer, const char *dial_status)
+{
+       /* In general, most things shouldn't get a dial end. */
+       ast_assert(0);
+       return 0;
+}
+
+static enum process_bridge_enter_results base_process_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel)
+{
+       /* Base process bridge enter simply indicates that we can't handle it */
+       return BRIDGE_ENTER_NEED_CDR;
+}
+
+static int base_process_parked_channel(struct cdr_object *cdr, struct ast_parked_call_payload *parking_info)
+{
+       char park_info[128];
+
+       ast_assert(!strcasecmp(parking_info->parkee->name, cdr->party_a.snapshot->name));
+
+       /* Update Party A information regardless */
+       cdr->fn_table->process_party_a(cdr, parking_info->parkee);
+
+       /* Fake out where we're parked */
+       ast_string_field_set(cdr, appl, "Park");
+       snprintf(park_info, sizeof(park_info), "%s:%u", parking_info->parkinglot, parking_info->parkingspace);
+       ast_string_field_set(cdr, data, park_info);
+
+       /* Prevent any further changes to the App/Data fields for this record */
+       ast_set_flag(&cdr->flags, AST_CDR_LOCK_APP);
+
+       return 0;
+}
+
+/* SINGLE STATE */
+
+static void single_state_init_function(struct cdr_object *cdr) {
+       cdr->start = ast_tvnow();
+       cdr_object_check_party_a_answer(cdr);
+}
+
+static void single_state_process_party_b(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot)
+{
+       /* This should never happen! */
+       ast_assert(cdr->party_b.snapshot == NULL);
+       ast_assert(0);
+       return;
+}
+
+static int single_state_process_dial_begin(struct cdr_object *cdr, struct ast_channel_snapshot *caller, struct ast_channel_snapshot *peer)
+{
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       if (caller && !strcasecmp(cdr->party_a.snapshot->name, caller->name)) {
+               base_process_party_a(cdr, caller);
+               CDR_DEBUG(mod_cfg, "%p - Updated Party A %s snapshot\n", cdr,
+                               cdr->party_a.snapshot->name);
+               cdr_object_swap_snapshot(&cdr->party_b, peer);
+               CDR_DEBUG(mod_cfg, "%p - Updated Party B %s snapshot\n", cdr,
+                               cdr->party_b.snapshot->name);
+
+               /* If we have two parties, lock the application that caused the
+                * two parties to be associated. This prevents mid-call event
+                * macros/gosubs from perturbing the CDR application/data
+                */
+               ast_set_flag(&cdr->flags, AST_CDR_LOCK_APP);
+       } else if (!strcasecmp(cdr->party_a.snapshot->name, peer->name)) {
+               /* We're the entity being dialed, i.e., outbound origination */
+               base_process_party_a(cdr, peer);
+               CDR_DEBUG(mod_cfg, "%p - Updated Party A %s snapshot\n", cdr,
+                               cdr->party_a.snapshot->name);
+       }
+
+       cdr_object_transition_state(cdr, &dial_state_fn_table);
+       return 0;
+}
+
+/*!
+ * \brief Handle a comparison between our \ref cdr_object and a \ref cdr_object
+ * already in the bridge while in the Single state. The goal of this is to find
+ * a Party B for our CDR.
+ *
+ * \param cdr Our \ref cdr_object in the Single state
+ * \param cand_cdr The \ref cdr_object already in the Bridge state
+ *
+ * \retval 0 The cand_cdr had a Party A or Party B that we could use as our
+ * Party B
+ * \retval 1 No party in the cand_cdr could be used as our Party B
+ */
+static int single_state_bridge_enter_comparison(struct cdr_object *cdr,
+               struct cdr_object *cand_cdr)
+{
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       struct cdr_object_snapshot *party_a;
+
+       /* Don't match on ourselves */
+       if (!strcasecmp(cdr->party_a.snapshot->name, cand_cdr->party_a.snapshot->name)) {
+               return 1;
+       }
+
+       /* Try the candidate CDR's Party A first */
+       party_a = cdr_object_pick_party_a(&cdr->party_a, &cand_cdr->party_a);
+       if (!strcasecmp(party_a->snapshot->name, cdr->party_a.snapshot->name)) {
+               CDR_DEBUG(mod_cfg, "%p - Party A %s has new Party B %s\n",
+                       cdr, cdr->party_a.snapshot->name, cand_cdr->party_a.snapshot->name);
+               cdr_object_snapshot_copy(&cdr->party_b, &cand_cdr->party_a);
+               if (!cand_cdr->party_b.snapshot) {
+                       /* We just stole them - finalize their CDR. Note that this won't
+                        * transition their state, it just sets the end time and the
+                        * disposition - if we need to re-activate them later, we can.
+                        */
+                       cdr_object_finalize(cand_cdr);
+               }
+               return 0;
+       }
+
+       /* Try their Party B, unless it's us */
+       if (!cand_cdr->party_b.snapshot
+               || !strcasecmp(cdr->party_a.snapshot->name, cand_cdr->party_b.snapshot->name)) {
+               return 1;
+       }
+       party_a = cdr_object_pick_party_a(&cdr->party_a, &cand_cdr->party_b);
+       if (!strcasecmp(party_a->snapshot->name, cdr->party_a.snapshot->name)) {
+               CDR_DEBUG(mod_cfg, "%p - Party A %s has new Party B %s\n",
+                       cdr, cdr->party_a.snapshot->name, cand_cdr->party_b.snapshot->name);
+               cdr_object_snapshot_copy(&cdr->party_b, &cand_cdr->party_b);
+               return 0;
+       }
+
+       return 1;
+}
+
+static enum process_bridge_enter_results single_state_process_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel)
+{
+       struct ao2_iterator it_cdrs;
+       char *channel_id;
+       int success = 0;
+
+       ast_string_field_set(cdr, bridge, bridge->uniqueid);
+
+       if (ao2_container_count(bridge->channels) == 1) {
+               /* No one in the bridge yet but us! */
+               cdr_object_transition_state(cdr, &bridge_state_fn_table);
+               return BRIDGE_ENTER_ONLY_PARTY;
+       }
+
+       for (it_cdrs = ao2_iterator_init(bridge->channels, 0);
+               !success && (channel_id = ao2_iterator_next(&it_cdrs));
+               ao2_ref(channel_id, -1)) {
+               RAII_VAR(struct cdr_object *, cand_cdr_master,
+                       ao2_find(active_cdrs_by_channel, channel_id, OBJ_KEY),
+                       ao2_cleanup);
+               struct cdr_object *cand_cdr;
+
+               if (!cand_cdr_master) {
+                       continue;
+               }
+
+               ao2_lock(cand_cdr_master);
+               for (cand_cdr = cand_cdr_master; cand_cdr; cand_cdr = cand_cdr->next) {
+                       /* Skip any records that are not in a bridge or in this bridge.
+                        * I'm not sure how that would happen, but it pays to be careful. */
+                       if (cand_cdr->fn_table != &bridge_state_fn_table ||
+                                       strcmp(cdr->bridge, cand_cdr->bridge)) {
+                               continue;
+                       }
+
+                       if (single_state_bridge_enter_comparison(cdr, cand_cdr)) {
+                               continue;
+                       }
+                       /* We successfully got a party B - break out */
+                       success = 1;
+                       break;
+               }
+               ao2_unlock(cand_cdr_master);
+       }
+       ao2_iterator_destroy(&it_cdrs);
+
+       /* We always transition state, even if we didn't get a peer */
+       cdr_object_transition_state(cdr, &bridge_state_fn_table);
+
+       /* Success implies that we have a Party B */
+       if (success) {
+               return BRIDGE_ENTER_OBTAINED_PARTY_B;
+       }
+
+       return BRIDGE_ENTER_NO_PARTY_B;
+}
+
+static int single_state_process_parking_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel)
+{
+       cdr_object_transition_state(cdr, &parked_state_fn_table);
+       return 0;
+}
+
+
+/* DIAL STATE */
+
+static void dial_state_process_party_b(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot)
+{
+       ast_assert(snapshot != NULL);
+
+       if (!cdr->party_b.snapshot
+               || strcasecmp(cdr->party_b.snapshot->name, snapshot->name)) {
+               return;
+       }
+       cdr_object_swap_snapshot(&cdr->party_b, snapshot);
+
+       /* If party B hangs up, finalize this CDR */
+       if (ast_test_flag(&cdr->party_b.snapshot->flags, AST_FLAG_DEAD)) {
+               cdr_object_transition_state(cdr, &finalized_state_fn_table);
+       }
+}
+
+static int dial_state_process_dial_begin(struct cdr_object *cdr, struct ast_channel_snapshot *caller, struct ast_channel_snapshot *peer)
+{
+       /* Don't process a begin dial here. A party A already in the dial state will
+        * who receives a dial begin for something else will be handled by the
+        * message router callback and will add a new CDR for the party A */
+       return 1;
+}
+
+/*!
+ * \internal
+ * \brief Convert a dial status to a CDR disposition
+ */
+static enum ast_cdr_disposition dial_status_to_disposition(const char *dial_status)
+{
+       RAII_VAR(struct module_config *, mod_cfg,
+               ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       if (!strcmp(dial_status, "ANSWER")) {
+               return AST_CDR_ANSWERED;
+       } else if (!strcmp(dial_status, "BUSY")) {
+               return AST_CDR_BUSY;
+       } else if (!strcmp(dial_status, "CANCEL") || !strcmp(dial_status, "NOANSWER")) {
+               return AST_CDR_NOANSWER;
+       } else if (!strcmp(dial_status, "CONGESTION")) {
+               if (!ast_test_flag(&mod_cfg->general->settings, CDR_CONGESTION)) {
+                       return AST_CDR_FAILED;
+               } else {
+                       return AST_CDR_CONGESTION;
+               }
+       } else if (!strcmp(dial_status, "FAILED")) {
+               return AST_CDR_FAILED;
+       }
+       return AST_CDR_FAILED;
+}
+
+static int dial_state_process_dial_end(struct cdr_object *cdr, struct ast_channel_snapshot *caller, struct ast_channel_snapshot *peer, const char *dial_status)
+{
+       struct ast_channel_snapshot *party_a;
+
+       if (caller) {
+               party_a = caller;
+       } else {
+               party_a = peer;
+       }
+       ast_assert(!strcasecmp(cdr->party_a.snapshot->name, party_a->name));
+       cdr_object_swap_snapshot(&cdr->party_a, party_a);
+
+       if (cdr->party_b.snapshot) {
+               if (strcasecmp(cdr->party_b.snapshot->name, peer->name)) {
+                       /* Not the status for this CDR - defer back to the message router */
+                       return 1;
+               }
+               cdr_object_swap_snapshot(&cdr->party_b, peer);
+       }
+
+       /* Set the disposition based on the dial string. */
+       cdr->disposition = dial_status_to_disposition(dial_status);
+       if (cdr->disposition == AST_CDR_ANSWERED) {
+               /* Switch to dial pending to wait and see what the caller does */
+               cdr_object_transition_state(cdr, &dialed_pending_state_fn_table);
+       } else {
+               cdr_object_transition_state(cdr, &finalized_state_fn_table);
+       }
+
+       return 0;
+}
+
+static enum process_bridge_enter_results dial_state_process_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel)
+{
+       struct ao2_iterator it_cdrs;
+       char *channel_id;
+       int success = 0;
+
+       ast_string_field_set(cdr, bridge, bridge->uniqueid);
+
+       /* Get parties in the bridge */
+       if (ao2_container_count(bridge->channels) == 1) {
+               /* No one in the bridge yet but us! */
+               cdr_object_transition_state(cdr, &bridge_state_fn_table);
+               return BRIDGE_ENTER_ONLY_PARTY;
+       }
+
+       for (it_cdrs = ao2_iterator_init(bridge->channels, 0);
+               !success && (channel_id = ao2_iterator_next(&it_cdrs));
+               ao2_ref(channel_id, -1)) {
+               RAII_VAR(struct cdr_object *, cand_cdr_master,
+                       ao2_find(active_cdrs_by_channel, channel_id, OBJ_KEY),
+                       ao2_cleanup);
+               struct cdr_object *cand_cdr;
+
+               if (!cand_cdr_master) {
+                       continue;
+               }
+
+               ao2_lock(cand_cdr_master);
+               for (cand_cdr = cand_cdr_master; cand_cdr; cand_cdr = cand_cdr->next) {
+                       /* Skip any records that are not in a bridge or in this bridge.
+                        * I'm not sure how that would happen, but it pays to be careful. */
+                       if (cand_cdr->fn_table != &bridge_state_fn_table ||
+                                       strcmp(cdr->bridge, cand_cdr->bridge)) {
+                               continue;
+                       }
+
+                       /* If we don't have a Party B (originated channel), skip it */
+                       if (!cdr->party_b.snapshot) {
+                               continue;
+                       }
+
+                       /* Skip any records that aren't our Party B */
+                       if (strcasecmp(cdr->party_b.snapshot->name, cand_cdr->party_a.snapshot->name)) {
+                               continue;
+                       }
+                       cdr_object_snapshot_copy(&cdr->party_b, &cand_cdr->party_a);
+                       /* If they have a Party B, they joined up with someone else as their
+                        * Party A. Don't finalize them as they're active. Otherwise, we
+                        * have stolen them so they need to be finalized.
+                        */
+                       if (!cand_cdr->party_b.snapshot) {
+                               cdr_object_finalize(cand_cdr);
+                       }
+                       success = 1;
+                       break;
+               }
+               ao2_unlock(cand_cdr_master);
+       }
+       ao2_iterator_destroy(&it_cdrs);
+
+       /* We always transition state, even if we didn't get a peer */
+       cdr_object_transition_state(cdr, &bridge_state_fn_table);
+
+       /* Success implies that we have a Party B */
+       if (success) {
+               return BRIDGE_ENTER_OBTAINED_PARTY_B;
+       }
+       return BRIDGE_ENTER_NO_PARTY_B;
+}
+
+/* DIALED PENDING STATE */
+
+static int dialed_pending_state_process_party_a(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot)
+{
+       /* If we get a CEP change, we're executing dialplan. If we have a Party B
+        * that means we need a new CDR; otherwise, switch us over to single.
+        */
+       if (snapshot_cep_changed(cdr->party_a.snapshot, snapshot)) {
+               if (cdr->party_b.snapshot) {
+                       cdr_object_transition_state(cdr, &finalized_state_fn_table);
+                       cdr->fn_table->process_party_a(cdr, snapshot);
+                       return 1;
+               } else {
+                       cdr_object_transition_state(cdr, &single_state_fn_table);
+                       cdr->fn_table->process_party_a(cdr, snapshot);
+                       return 0;
+               }
+       }
+       base_process_party_a(cdr, snapshot);
+       return 0;
+}
+
+static enum process_bridge_enter_results dialed_pending_state_process_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel)
+{
+       cdr_object_transition_state(cdr, &dial_state_fn_table);
+       return cdr->fn_table->process_bridge_enter(cdr, bridge, channel);
+}
+
+static int dialed_pending_state_process_parking_bridge_enter(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel)
+{
+       if (cdr->party_b.snapshot) {
+               /* We can't handle this as we have a Party B - ask for a new one */
+               return 1;
+       }
+       cdr_object_transition_state(cdr, &parked_state_fn_table);
+       return 0;
+}
+
+static int dialed_pending_state_process_dial_begin(struct cdr_object *cdr, struct ast_channel_snapshot *caller, struct ast_channel_snapshot *peer)
+{
+       cdr_object_transition_state(cdr, &finalized_state_fn_table);
+
+       /* Ask for a new CDR */
+       return 1;
+}
+
+/* BRIDGE STATE */
+
+static void bridge_state_process_party_b(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot)
+{
+       if (!cdr->party_b.snapshot
+               || strcasecmp(cdr->party_b.snapshot->name, snapshot->name)) {
+               return;
+       }
+       cdr_object_swap_snapshot(&cdr->party_b, snapshot);
+
+       /* If party B hangs up, finalize this CDR */
+       if (ast_test_flag(&cdr->party_b.snapshot->flags, AST_FLAG_DEAD)) {
+               cdr_object_transition_state(cdr, &finalized_state_fn_table);
+       }
+}
+
+static int bridge_state_process_bridge_leave(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel)
+{
+       if (strcmp(cdr->bridge, bridge->uniqueid)) {
+               return 1;
+       }
+       if (strcasecmp(cdr->party_a.snapshot->name, channel->name)
+               && cdr->party_b.snapshot
+               && strcasecmp(cdr->party_b.snapshot->name, channel->name)) {
+               return 1;
+       }
+       cdr_object_transition_state(cdr, &finalized_state_fn_table);
+
+       return 0;
+}
+
+/* PARKED STATE */
+
+static int parked_state_process_bridge_leave(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge, struct ast_channel_snapshot *channel)
+{
+       if (strcasecmp(cdr->party_a.snapshot->name, channel->name)) {
+               return 1;
+       }
+       cdr_object_transition_state(cdr, &finalized_state_fn_table);
+
+       return 0;
+}
+
+/* FINALIZED STATE */
+
+static void finalized_state_init_function(struct cdr_object *cdr)
+{
+       cdr_object_finalize(cdr);
+}
+
+static int finalized_state_process_party_a(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot)
+{
+       RAII_VAR(struct module_config *, mod_cfg,
+               ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       if (ast_test_flag(&snapshot->softhangup_flags, AST_SOFTHANGUP_HANGUP_EXEC)
+                       && ast_test_flag(&mod_cfg->general->settings, CDR_END_BEFORE_H_EXTEN)) {
+               return 0;
+       }
+
+       /* Indicate that, if possible, we should get a new CDR */
+       return 1;
+}
+
+/*!
+ * \internal
+ * \brief Filter channel snapshots by technology
+ */
+static int filter_channel_snapshot(struct ast_channel_snapshot *snapshot)
+{
+       return snapshot->tech_properties & AST_CHAN_TP_INTERNAL;
+}
+
+/*!
+ * \internal
+ * \brief Filter a channel cache update
+ */
+static int filter_channel_cache_message(struct ast_channel_snapshot *old_snapshot,
+               struct ast_channel_snapshot *new_snapshot)
+{
+       int ret = 0;
+
+       /* Drop cache updates from certain channel technologies */
+       if (old_snapshot) {
+               ret |= filter_channel_snapshot(old_snapshot);
+       }
+       if (new_snapshot) {
+               ret |= filter_channel_snapshot(new_snapshot);
+       }
+
+       return ret;
+}
+
+/* TOPIC ROUTER CALLBACKS */
+
+/*!
+ * \brief Handler for Stasis-Core dial messages
+ * \param data Passed on
+ * \param sub The stasis subscription for this message callback
+ * \param topic The topic this message was published for
+ * \param message The message
+ */
+static void handle_dial_message(void *data, struct stasis_subscription *sub, struct stasis_message *message)
+{
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       RAII_VAR(struct cdr_object *, cdr, NULL, ao2_cleanup);
+       struct ast_multi_channel_blob *payload = stasis_message_data(message);
+       struct ast_channel_snapshot *caller;
+       struct ast_channel_snapshot *peer;
+       struct cdr_object *it_cdr;
+       struct ast_json *dial_status_blob;
+       const char *dial_status = NULL;
+       int res = 1;
+
+       caller = ast_multi_channel_blob_get_channel(payload, "caller");
+       peer = ast_multi_channel_blob_get_channel(payload, "peer");
+       if (!peer && !caller) {
+               return;
+       }
+       dial_status_blob = ast_json_object_get(ast_multi_channel_blob_get_json(payload), "dialstatus");
+       if (dial_status_blob) {
+               dial_status = ast_json_string_get(dial_status_blob);
+       }
+
+       CDR_DEBUG(mod_cfg, "Dial %s message for %s, %s: %u.%08u\n",
+                       ast_strlen_zero(dial_status) ? "Begin" : "End",
+                       caller ? caller->name : "(none)",
+                       peer ? peer->name : "(none)",
+                       (unsigned int)stasis_message_timestamp(message)->tv_sec,
+                       (unsigned int)stasis_message_timestamp(message)->tv_usec);
+
+       if (filter_channel_snapshot(peer) || (caller && filter_channel_snapshot(caller))) {
+               return;
+       }
+
+       /* Figure out who is running this show */
+       if (caller) {
+               cdr = ao2_find(active_cdrs_by_channel, caller->uniqueid, OBJ_KEY);
+       } else {
+               cdr = ao2_find(active_cdrs_by_channel, peer->uniqueid, OBJ_KEY);
+       }
+
+       if (!cdr) {
+               ast_log(AST_LOG_WARNING, "No CDR for channel %s\n", caller ? caller->name : peer->name);
+               ast_assert(0);
+               return;
+       }
+
+       ao2_lock(cdr);
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (ast_strlen_zero(dial_status)) {
+                       if (!it_cdr->fn_table->process_dial_begin) {
+                               continue;
+                       }
+                       CDR_DEBUG(mod_cfg, "%p - Processing Dial Begin message for channel %s, peer %s\n",
+                                       it_cdr,
+                                       caller ? caller->name : "(none)",
+                                       peer ? peer->name : "(none)");
+                       res &= it_cdr->fn_table->process_dial_begin(it_cdr,
+                                       caller,
+                                       peer);
+               } else {
+                       if (!it_cdr->fn_table->process_dial_end) {
+                               continue;
+                       }
+                       CDR_DEBUG(mod_cfg, "%p - Processing Dial End message for channel %s, peer %s\n",
+                                       it_cdr,
+                                       caller ? caller->name : "(none)",
+                                       peer ? peer->name : "(none)");
+                       it_cdr->fn_table->process_dial_end(it_cdr,
+                                       caller,
+                                       peer,
+                                       dial_status);
+               }
+       }
+
+       /* If no CDR handled a dial begin message, make a new one */
+       if (res && ast_strlen_zero(dial_status)) {
+               struct cdr_object *new_cdr;
+
+               new_cdr = cdr_object_create_and_append(cdr);
+               if (!new_cdr) {
+                       ao2_unlock(cdr);
+                       return;
+               }
+               new_cdr->fn_table->process_dial_begin(new_cdr,
+                               caller,
+                               peer);
+       }
+       ao2_unlock(cdr);
+}
+
+static int cdr_object_finalize_party_b(void *obj, void *arg, int flags)
+{
+       struct cdr_object *cdr = obj;
+       struct ast_channel_snapshot *party_b = arg;
+       struct cdr_object *it_cdr;
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (it_cdr->party_b.snapshot
+                       && !strcasecmp(it_cdr->party_b.snapshot->name, party_b->name)) {
+                       /* Don't transition to the finalized state - let the Party A do
+                        * that when its ready
+                        */
+                       cdr_object_finalize(it_cdr);
+               }
+       }
+       return 0;
+}
+
+static int cdr_object_update_party_b(void *obj, void *arg, int flags)
+{
+       struct cdr_object *cdr = obj;
+       struct ast_channel_snapshot *party_b = arg;
+       struct cdr_object *it_cdr;
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (!it_cdr->fn_table->process_party_b) {
+                       continue;
+               }
+               if (it_cdr->party_b.snapshot
+                       && !strcasecmp(it_cdr->party_b.snapshot->name, party_b->name)) {
+                       it_cdr->fn_table->process_party_b(it_cdr, party_b);
+               }
+       }
+       return 0;
+}
+
+/*! \brief Determine if we need to add a new CDR based on snapshots */
+static int check_new_cdr_needed(struct ast_channel_snapshot *old_snapshot,
+               struct ast_channel_snapshot *new_snapshot)
+{
+       RAII_VAR(struct module_config *, mod_cfg,
+                       ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       /* If we're dead, we don't need a new CDR */
+       if (!new_snapshot
+               || (ast_test_flag(&new_snapshot->softhangup_flags, AST_SOFTHANGUP_HANGUP_EXEC)
+                       && ast_test_flag(&mod_cfg->general->settings, CDR_END_BEFORE_H_EXTEN))) {
+               return 0;
+       }
+
+       /* Auto-fall through will increment the priority but have no application */
+       if (ast_strlen_zero(new_snapshot->appl)) {
+               return 0;
+       }
+
+       if (old_snapshot && !snapshot_cep_changed(old_snapshot, new_snapshot)) {
+               return 0;
+       }
+
+       return 1;
+}
+
+/*!
+ * \brief Handler for Stasis-Core channel cache update messages
+ * \param data Passed on
+ * \param sub The stasis subscription for this message callback
+ * \param topic The topic this message was published for
+ * \param message The message
+ */
+static void handle_channel_cache_message(void *data, struct stasis_subscription *sub, struct stasis_message *message)
+{
+       RAII_VAR(struct cdr_object *, cdr, NULL, ao2_cleanup);
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       struct stasis_cache_update *update = stasis_message_data(message);
+       struct ast_channel_snapshot *old_snapshot;
+       struct ast_channel_snapshot *new_snapshot;
+       const char *uniqueid;
+       const char *name;
+       struct cdr_object *it_cdr;
+
+       ast_assert(update != NULL);
+       ast_assert(ast_channel_snapshot_type() == update->type);
+
+       old_snapshot = stasis_message_data(update->old_snapshot);
+       new_snapshot = stasis_message_data(update->new_snapshot);
+       uniqueid = new_snapshot ? new_snapshot->uniqueid : old_snapshot->uniqueid;
+       name = new_snapshot ? new_snapshot->name : old_snapshot->name;
+
+       if (filter_channel_cache_message(old_snapshot, new_snapshot)) {
+               return;
+       }
+
+       if (new_snapshot && !old_snapshot) {
+               cdr = cdr_object_alloc(new_snapshot);
+               if (!cdr) {
+                       return;
+               }
+               ao2_link(active_cdrs_by_channel, cdr);
+       }
+
+       /* Handle Party A */
+       if (!cdr) {
+               cdr = ao2_find(active_cdrs_by_channel, uniqueid, OBJ_KEY);
+       }
+       if (!cdr) {
+               ast_log(AST_LOG_WARNING, "No CDR for channel %s\n", name);
+               ast_assert(0);
+       } else {
+               ao2_lock(cdr);
+               if (new_snapshot) {
+                       int all_reject = 1;
+                       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+                               if (!it_cdr->fn_table->process_party_a) {
+                                       continue;
+                               }
+                               all_reject &= it_cdr->fn_table->process_party_a(it_cdr, new_snapshot);
+                       }
+                       if (all_reject && check_new_cdr_needed(old_snapshot, new_snapshot)) {
+                               /* We're not hung up and we have a new snapshot - we need a new CDR */
+                               struct cdr_object *new_cdr;
+                               new_cdr = cdr_object_create_and_append(cdr);
+                               if (new_cdr) {
+                                       new_cdr->fn_table->process_party_a(new_cdr, new_snapshot);
+                               }
+                       }
+               } else {
+                       CDR_DEBUG(mod_cfg, "%p - Beginning finalize/dispatch for %s\n", cdr, old_snapshot->name);
+                       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+                               cdr_object_finalize(it_cdr);
+                       }
+                       cdr_object_dispatch(cdr);
+                       ao2_unlink(active_cdrs_by_channel, cdr);
+               }
+               ao2_unlock(cdr);
+       }
+
+       /* Handle Party B */
+       if (new_snapshot) {
+               ao2_callback(active_cdrs_by_channel, OBJ_NODATA, cdr_object_update_party_b,
+                       new_snapshot);
+       } else {
+               ao2_callback(active_cdrs_by_channel, OBJ_NODATA, cdr_object_finalize_party_b,
+                       old_snapshot);
+       }
+
+}
+
+struct bridge_leave_data {
+       struct ast_bridge_snapshot *bridge;
+       struct ast_channel_snapshot *channel;
+};
+
+/*! \brief Callback used to notify CDRs of a Party B leaving the bridge */
+static int cdr_object_party_b_left_bridge_cb(void *obj, void *arg, int flags)
+{
+       struct cdr_object *cdr = obj;
+       struct bridge_leave_data *leave_data = arg;
+       struct cdr_object *it_cdr;
+
+       if (strcmp(cdr->bridge, leave_data->bridge->uniqueid)) {
+               return 0;
+       }
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (it_cdr->fn_table != &bridge_state_fn_table) {
+                       continue;
+               }
+               if (!it_cdr->party_b.snapshot) {
+                       continue;
+               }
+               if (strcasecmp(it_cdr->party_b.snapshot->name, leave_data->channel->name)) {
+                       continue;
+               }
+               /* It is our Party B, in our bridge. Set the end time and let the handler
+                * transition our CDR appropriately when we leave the bridge.
+                */
+               cdr_object_finalize(it_cdr);
+       }
+       return 0;
+}
+
+/*! \brief Filter bridge messages based on bridge technology */
+static int filter_bridge_messages(struct ast_bridge_snapshot *bridge)
+{
+       /* Ignore holding bridge technology messages. We treat this simply as an application
+        * that a channel enters into.
+        */
+       if (!strcmp(bridge->technology, "holding_bridge") && strcmp(bridge->subclass, "parking")) {
+               return 1;
+       }
+       return 0;
+}
+
+/*!
+ * \brief Handler for when a channel leaves a bridge
+ * \param data Passed on
+ * \param sub The stasis subscription for this message callback
+ * \param topic The topic this message was published for
+ * \param message The message - hopefully a bridge one!
+ */
+static void handle_bridge_leave_message(void *data, struct stasis_subscription *sub,
+               struct stasis_message *message)
+{
+       struct ast_bridge_blob *update = stasis_message_data(message);
+       struct ast_bridge_snapshot *bridge = update->bridge;
+       struct ast_channel_snapshot *channel = update->channel;
+       RAII_VAR(struct module_config *, mod_cfg,
+                       ao2_global_obj_ref(module_configs), ao2_cleanup);
+       RAII_VAR(struct cdr_object *, cdr,
+                       ao2_find(active_cdrs_by_channel, channel->uniqueid, OBJ_KEY),
+                       ao2_cleanup);
+       struct cdr_object *it_cdr;
+       struct bridge_leave_data leave_data = {
+               .bridge = bridge,
+               .channel = channel,
+       };
+       int left_bridge = 0;
+
+       if (filter_bridge_messages(bridge)) {
+               return;
+       }
+
+       if (filter_channel_snapshot(channel)) {
+               return;
+       }
+
+       CDR_DEBUG(mod_cfg, "Bridge Leave message for %s: %u.%08u\n",
+                       channel->name,
+                       (unsigned int)stasis_message_timestamp(message)->tv_sec,
+                       (unsigned int)stasis_message_timestamp(message)->tv_usec);
+
+       if (!cdr) {
+               ast_log(AST_LOG_WARNING, "No CDR for channel %s\n", channel->name);
+               ast_assert(0);
+               return;
+       }
+
+       /* Party A */
+       ao2_lock(cdr);
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (!it_cdr->fn_table->process_bridge_leave) {
+                       continue;
+               }
+               CDR_DEBUG(mod_cfg, "%p - Processing Bridge Leave for %s\n",
+                               it_cdr, channel->name);
+               if (!it_cdr->fn_table->process_bridge_leave(it_cdr, bridge, channel)) {
+                       ast_string_field_set(it_cdr, bridge, "");
+                       left_bridge = 1;
+               }
+       }
+       ao2_unlock(cdr);
+       if (!left_bridge) {
+               return;
+       }
+
+       if (strcmp(bridge->subclass, "parking")) {
+               /* Party B */
+               ao2_callback(active_cdrs_by_channel, OBJ_NODATA,
+                               cdr_object_party_b_left_bridge_cb,
+                               &leave_data);
+       }
+}
+
+/*!
+ * \internal
+ * \brief Create a new CDR, append it to an existing CDR, and update its snapshots
+ *
+ * \note The new CDR will be automatically transitioned to the bridge state
+ */
+static void bridge_candidate_add_to_cdr(struct cdr_object *cdr,
+               struct cdr_object_snapshot *party_b)
+{
+       RAII_VAR(struct module_config *,  mod_cfg,
+               ao2_global_obj_ref(module_configs), ao2_cleanup);
+       struct cdr_object *new_cdr;
+
+       new_cdr = cdr_object_create_and_append(cdr);
+       if (!new_cdr) {
+               return;
+       }
+       cdr_object_snapshot_copy(&new_cdr->party_b, party_b);
+       cdr_object_check_party_a_answer(new_cdr);
+       ast_string_field_set(new_cdr, bridge, cdr->bridge);
+       cdr_object_transition_state(new_cdr, &bridge_state_fn_table);
+       CDR_DEBUG(mod_cfg, "%p - Party A %s has new Party B %s\n",
+               new_cdr, new_cdr->party_a.snapshot->name,
+               party_b->snapshot->name);
+}
+
+/*!
+ * \brief Process a single \ref bridge_candidate
+ *
+ * When a CDR enters a bridge, it needs to make pairings with everyone else
+ * that it is not currently paired with. This function determines, for the
+ * CDR for the channel that entered the bridge and the CDR for every other
+ * channel currently in the bridge, who is Party A and makes new CDRs.
+ *
+ * \param cdr The \ref cdr_obj being processed
+ * \param cand_cdr The \ref cdr_object that is a candidate
+ *
+ */
+static int bridge_candidate_process(struct cdr_object *cdr, struct cdr_object *base_cand_cdr)
+{
+       RAII_VAR(struct module_config *, mod_cfg,
+               ao2_global_obj_ref(module_configs), ao2_cleanup);
+       struct cdr_object_snapshot *party_a;
+       struct cdr_object *cand_cdr;
+
+       SCOPED_AO2LOCK(lock, base_cand_cdr);
+
+       for (cand_cdr = base_cand_cdr; cand_cdr; cand_cdr = cand_cdr->next) {
+               /* Skip any records that are not in this bridge */
+               if (strcmp(cand_cdr->bridge, cdr->bridge)) {
+                       continue;
+               }
+
+               /* If the candidate is us or someone we've taken on, pass on by */
+               if (!strcasecmp(cdr->party_a.snapshot->name, cand_cdr->party_a.snapshot->name)
+                       || (cdr->party_b.snapshot
+                               && !strcasecmp(cdr->party_b.snapshot->name, cand_cdr->party_a.snapshot->name))) {
+                       return 0;
+               }
+
+               party_a = cdr_object_pick_party_a(&cdr->party_a, &cand_cdr->party_a);
+               /* We're party A - make a new CDR, append it to us, and set the candidate as
+                * Party B */
+               if (!strcasecmp(party_a->snapshot->name, cdr->party_a.snapshot->name)) {
+                       bridge_candidate_add_to_cdr(cdr, &cand_cdr->party_a);
+                       return 0;
+               }
+
+               /* We're Party B. Check if we can add ourselves immediately or if we need
+                * a new CDR for them (they already have a Party B) */
+               if (cand_cdr->party_b.snapshot
+                       && strcasecmp(cand_cdr->party_b.snapshot->name, cdr->party_a.snapshot->name)) {
+                       bridge_candidate_add_to_cdr(cand_cdr, &cdr->party_a);
+               } else {
+                       CDR_DEBUG(mod_cfg, "%p - Party A %s has new Party B %s\n",
+                               cand_cdr, cand_cdr->party_a.snapshot->name,
+                               cdr->party_a.snapshot->name);
+                       cdr_object_snapshot_copy(&cand_cdr->party_b, &cdr->party_a);
+                       /* It's possible that this joined at one point and was never chosen
+                        * as party A. Clear their end time, as it would be set in such a
+                        * case.
+                        */
+                       memset(&cand_cdr->end, 0, sizeof(cand_cdr->end));
+               }
+               return 0;
+       }
+       return 0;
+}
+
+/*!
+ * \brief Handle creating bridge pairings for the \ref cdr_object that just
+ * entered a bridge
+ * \param cdr The \ref cdr_object that just entered the bridge
+ * \param bridge The \ref ast_bridge_snapshot representing the bridge it just entered
+ */
+static void handle_bridge_pairings(struct cdr_object *cdr, struct ast_bridge_snapshot *bridge)
+{
+       struct ao2_iterator it_channels;
+       char *channel_id;
+
+       it_channels = ao2_iterator_init(bridge->channels, 0);
+       while ((channel_id = ao2_iterator_next(&it_channels))) {
+               RAII_VAR(struct cdr_object *, cand_cdr,
+                       ao2_find(active_cdrs_by_channel, channel_id, OBJ_KEY),
+                       ao2_cleanup);
+
+               if (!cand_cdr) {
+                       ao2_ref(channel_id, -1);
+                       continue;
+               }
+
+               bridge_candidate_process(cdr, cand_cdr);
+
+               ao2_ref(channel_id, -1);
+       }
+       ao2_iterator_destroy(&it_channels);
+}
+
+/*! \brief Handle entering into a parking bridge
+ * \param cdr The CDR to operate on
+ * \param bridge The bridge the channel just entered
+ * \param channel The channel snapshot
+ */
+static void handle_parking_bridge_enter_message(struct cdr_object *cdr,
+               struct ast_bridge_snapshot *bridge,
+               struct ast_channel_snapshot *channel)
+{
+       RAII_VAR(struct module_config *, mod_cfg,
+                       ao2_global_obj_ref(module_configs), ao2_cleanup);
+       int res = 1;
+       struct cdr_object *it_cdr;
+       struct cdr_object *new_cdr;
+
+       ao2_lock(cdr);
+
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (it_cdr->fn_table->process_parking_bridge_enter) {
+                       res &= it_cdr->fn_table->process_parking_bridge_enter(it_cdr, bridge, channel);
+               }
+               if (it_cdr->fn_table->process_party_a) {
+                       CDR_DEBUG(mod_cfg, "%p - Updating Party A %s snapshot\n", it_cdr,
+                                       channel->name);
+                       it_cdr->fn_table->process_party_a(it_cdr, channel);
+               }
+       }
+
+       if (res) {
+               /* No one handled it - we need a new one! */
+               new_cdr = cdr_object_create_and_append(cdr);
+               if (new_cdr) {
+                       /* Let the single state transition us to Parked */
+                       cdr_object_transition_state(new_cdr, &single_state_fn_table);
+                       new_cdr->fn_table->process_parking_bridge_enter(new_cdr, bridge, channel);
+               }
+       }
+       ao2_unlock(cdr);
+}
+
+/*! \brief Handle a bridge enter message for a 'normal' bridge
+ * \param cdr The CDR to operate on
+ * \param bridge The bridge the channel just entered
+ * \param channel The channel snapshot
+ */
+static void handle_standard_bridge_enter_message(struct cdr_object *cdr,
+               struct ast_bridge_snapshot *bridge,
+               struct ast_channel_snapshot *channel)
+{
+       RAII_VAR(struct module_config *, mod_cfg,
+                       ao2_global_obj_ref(module_configs), ao2_cleanup);
+       enum process_bridge_enter_results result;
+       struct cdr_object *it_cdr;
+       struct cdr_object *new_cdr;
+       struct cdr_object *handled_cdr = NULL;
+
+       ao2_lock(cdr);
+
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (it_cdr->fn_table->process_party_a) {
+                       CDR_DEBUG(mod_cfg, "%p - Updating Party A %s snapshot\n", it_cdr,
+                                       channel->name);
+                       it_cdr->fn_table->process_party_a(it_cdr, channel);
+               }
+
+               /* Notify all states that they have entered a bridge */
+               if (it_cdr->fn_table->process_bridge_enter) {
+                       CDR_DEBUG(mod_cfg, "%p - Processing bridge enter for %s\n", it_cdr,
+                                       channel->name);
+                       result = it_cdr->fn_table->process_bridge_enter(it_cdr, bridge, channel);
+                       switch (result) {
+                       case BRIDGE_ENTER_ONLY_PARTY:
+                               /* Fall through */
+                       case BRIDGE_ENTER_OBTAINED_PARTY_B:
+                               if (!handled_cdr) {
+                                       handled_cdr = it_cdr;
+                               }
+                               break;
+                       case BRIDGE_ENTER_NEED_CDR:
+                               /* Pass */
+                               break;
+                       case BRIDGE_ENTER_NO_PARTY_B:
+                               /* We didn't win on any - end this CDR. If someone else comes in later
+                                * that is Party B to this CDR, it can re-activate this CDR.
+                                */
+                               if (!handled_cdr) {
+                                       handled_cdr = it_cdr;
+                               }
+                               cdr_object_finalize(cdr);
                                break;
                        }
+               }
+       }
 
-                       total++;
+       /* Create the new matchings, but only for either:
+        *  * The first CDR in the chain that handled it. This avoids issues with
+        *    forked CDRs.
+        *  * If no one handled it, the last CDR in the chain. This would occur if
+        *    a CDR joined a bridge and it wasn't Party A for anyone. We still need
+        *    to make pairings with everyone in the bridge.
+        */
+       if (handled_cdr) {
+               handle_bridge_pairings(handled_cdr, bridge);
+       } else {
+               /* Nothing handled it - we need a new one! */
+               new_cdr = cdr_object_create_and_append(cdr);
+               if (new_cdr) {
+                       /* This is guaranteed to succeed: the new CDR is created in the single state
+                        * and will be able to handle the bridge enter message
+                        */
+                       handle_standard_bridge_enter_message(cdr, bridge, channel);
                }
+       }
+       ao2_unlock(cdr);
+}
 
-               for (i = 0; cdr_readonly_vars[i]; i++) {
-                       workspace[0] = 0; /* null out the workspace, because the cdr_get_tv() won't write anything if time is NULL, so you get old vals */
-                       ast_cdr_getvar(cdr, cdr_readonly_vars[i], &tmp, workspace, sizeof(workspace), 0, 0);
-                       if (!tmp)
-                               continue;
+/*!
+ * \internal
+ * \brief Handler for Stasis-Core bridge enter messages
+ * \param data Passed on
+ * \param sub The stasis subscription for this message callback
+ * \param topic The topic this message was published for
+ * \param message The message - hopefully a bridge one!
+ */
+static void handle_bridge_enter_message(void *data, struct stasis_subscription *sub,
+               struct stasis_message *message)
+{
+       struct ast_bridge_blob *update = stasis_message_data(message);
+       struct ast_bridge_snapshot *bridge = update->bridge;
+       struct ast_channel_snapshot *channel = update->channel;
+       RAII_VAR(struct cdr_object *, cdr,
+                       ao2_find(active_cdrs_by_channel, channel->uniqueid, OBJ_KEY),
+                       ao2_cleanup);
+       RAII_VAR(struct module_config *, mod_cfg,
+                       ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       if (filter_bridge_messages(bridge)) {
+               return;
+       }
 
-                       if (ast_str_append(buf, 0, "level %d: %s%c%s%c", x, cdr_readonly_vars[i], delim, tmp, sep) < 0) {
-                               ast_log(LOG_ERROR, "Data Buffer Size Exceeded!\n");
-                               break;
-                       } else
-                               total++;
+       if (filter_channel_snapshot(channel)) {
+               return;
+       }
+
+       CDR_DEBUG(mod_cfg, "Bridge Enter message for channel %s: %u.%08u\n",
+                       channel->name,
+                       (unsigned int)stasis_message_timestamp(message)->tv_sec,
+                       (unsigned int)stasis_message_timestamp(message)->tv_usec);
+
+       if (!cdr) {
+               ast_log(AST_LOG_WARNING, "No CDR for channel %s\n", channel->name);
+               ast_assert(0);
+               return;
+       }
+
+       if (!strcmp(bridge->subclass, "parking")) {
+               handle_parking_bridge_enter_message(cdr, bridge, channel);
+       } else {
+               handle_standard_bridge_enter_message(cdr, bridge, channel);
+       }
+}
+
+/*!
+ * \brief Handler for when a channel is parked
+ * \param data Passed on
+ * \param sub The stasis subscription for this message callback
+ * \param topic The topic this message was published for
+ * \param message The message about who got parked
+ * */
+static void handle_parked_call_message(void *data, struct stasis_subscription *sub,
+               struct stasis_message *message)
+{
+       struct ast_parked_call_payload *payload = stasis_message_data(message);
+       struct ast_channel_snapshot *channel = payload->parkee;
+       RAII_VAR(struct cdr_object *, cdr, NULL, ao2_cleanup);
+       RAII_VAR(struct module_config *, mod_cfg,
+                       ao2_global_obj_ref(module_configs), ao2_cleanup);
+       int unhandled = 1;
+       struct cdr_object *it_cdr;
+
+       /* Anything other than getting parked will be handled by other updates */
+       if (payload->event_type != PARKED_CALL) {
+               return;
+       }
+
+       /* No one got parked? */
+       if (!channel) {
+               return;
+       }
+
+       if (filter_channel_snapshot(channel)) {
+               return;
+       }
+
+       CDR_DEBUG(mod_cfg, "Parked Call message for channel %s: %u.%08u\n",
+                       channel->name,
+                       (unsigned int)stasis_message_timestamp(message)->tv_sec,
+                       (unsigned int)stasis_message_timestamp(message)->tv_usec);
+
+       cdr = ao2_find(active_cdrs_by_channel, channel->uniqueid, OBJ_KEY);
+       if (!cdr) {
+               ast_log(AST_LOG_WARNING, "No CDR for channel %s\n", channel->name);
+               ast_assert(0);
+               return;
+       }
+
+       ao2_lock(cdr);
+
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (it_cdr->fn_table->process_parked_channel) {
+                       unhandled &= it_cdr->fn_table->process_parked_channel(it_cdr, payload);
                }
        }
 
-       return total;
+       if (unhandled) {
+               /* Nothing handled the messgae - we need a new one! */
+               struct cdr_object *new_cdr = cdr_object_create_and_append(cdr);
+               if (new_cdr) {
+                       /* As the new CDR is created in the single state, it is guaranteed
+                        * to have a function for the parked call message and will handle
+                        * the message */
+                       new_cdr->fn_table->process_parked_channel(new_cdr, payload);
+               }
+       }
+
+       ao2_unlock(cdr);
+
+}
+
+/*!
+ * \brief Handler for a synchronization message
+ * \param data Passed on
+ * \param sub The stasis subscription for this message callback
+ * \param topic The topic this message was published for
+ * \param message A blank ao2 object
+ * */
+static void handle_cdr_sync_message(void *data, struct stasis_subscription *sub,
+               struct stasis_message *message)
+{
+       return;
+}
+
+struct ast_cdr_config *ast_cdr_get_config(void)
+{
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       ao2_ref(mod_cfg->general, +1);
+       return mod_cfg->general;
+}
+
+void ast_cdr_set_config(struct ast_cdr_config *config)
+{
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       ao2_cleanup(mod_cfg->general);
+       mod_cfg->general = config;
+       ao2_ref(mod_cfg->general, +1);
+
+       cdr_toggle_runtime_options();
 }
 
+int ast_cdr_is_enabled(void)
+{
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       return ast_test_flag(&mod_cfg->general->settings, CDR_ENABLED);
+}
 
-void ast_cdr_free_vars(struct ast_cdr *cdr, int recur)
+int ast_cdr_backend_suspend(const char *name)
 {
+       int success = -1;
+       struct cdr_beitem *i = NULL;
 
-       /* clear variables */
-       for (; cdr; cdr = recur ? cdr->next : NULL) {
-               struct ast_var_t *vardata;
-               struct varshead *headp = &cdr->varshead;
-               while ((vardata = AST_LIST_REMOVE_HEAD(headp, entries)))
-                       ast_var_delete(vardata);
+       AST_RWLIST_WRLOCK(&be_list);
+       AST_RWLIST_TRAVERSE(&be_list, i, list) {
+               if (!strcasecmp(name, i->name)) {
+                       ast_debug(3, "Suspending CDR backend %s\n", i->name);
+                       i->suspended = 1;
+                       success = 0;
+               }
        }
+       AST_RWLIST_UNLOCK(&be_list);
+
+       return success;
 }
 
-/*! \brief  print a warning if cdr already posted */
-static void check_post(struct ast_cdr *cdr)
+int ast_cdr_backend_unsuspend(const char *name)
 {
-       if (!cdr)
-               return;
-       if (ast_test_flag(cdr, AST_CDR_FLAG_POSTED))
-               ast_log(LOG_NOTICE, "CDR on channel '%s' already posted\n", S_OR(cdr->channel, "<unknown>"));
+       int success = -1;
+       struct cdr_beitem *i = NULL;
+
+       AST_RWLIST_WRLOCK(&be_list);
+       AST_RWLIST_TRAVERSE(&be_list, i, list) {
+               if (!strcasecmp(name, i->name)) {
+                       ast_debug(3, "Unsuspending CDR backend %s\n", i->name);
+                       i->suspended = 0;
+                       success = 0;
+               }
+       }
+       AST_RWLIST_UNLOCK(&be_list);
+
+       return success;
 }
 
-void ast_cdr_free(struct ast_cdr *cdr)
+int ast_cdr_register(const char *name, const char *desc, ast_cdrbe be)
 {
+       struct cdr_beitem *i = NULL;
 
-       while (cdr) {
-               struct ast_cdr *next = cdr->next;
+       if (!name)
+               return -1;
 
-               ast_cdr_free_vars(cdr, 0);
-               ast_free(cdr);
-               cdr = next;
+       if (!be) {
+               ast_log(LOG_WARNING, "CDR engine '%s' lacks backend\n", name);
+               return -1;
+       }
+
+       AST_RWLIST_WRLOCK(&be_list);
+       AST_RWLIST_TRAVERSE(&be_list, i, list) {
+               if (!strcasecmp(name, i->name)) {
+                       ast_log(LOG_WARNING, "Already have a CDR backend called '%s'\n", name);
+                       AST_RWLIST_UNLOCK(&be_list);
+                       return -1;
+               }
        }
+
+       if (!(i = ast_calloc(1, sizeof(*i))))
+               return -1;
+
+       i->be = be;
+       ast_copy_string(i->name, name, sizeof(i->name));
+       ast_copy_string(i->desc, desc, sizeof(i->desc));
+
+       AST_RWLIST_INSERT_HEAD(&be_list, i, list);
+       AST_RWLIST_UNLOCK(&be_list);
+
+       return 0;
 }
 
-/*! \brief the same as a cdr_free call, only with no checks; just get rid of it */
-void ast_cdr_discard(struct ast_cdr *cdr)
+int ast_cdr_unregister(const char *name)
 {
-       while (cdr) {
-               struct ast_cdr *next = cdr->next;
+       struct cdr_beitem *match = NULL;
+       int active_count;
 
-               ast_cdr_free_vars(cdr, 0);
-               ast_free(cdr);
-               cdr = next;
+       AST_RWLIST_WRLOCK(&be_list);
+       AST_RWLIST_TRAVERSE(&be_list, match, list) {
+               if (!strcasecmp(name, match->name)) {
+                       break;
+               }
+       }
+
+       if (!match) {
+               AST_RWLIST_UNLOCK(&be_list);
+               return 0;
+       }
+
+       active_count = ao2_container_count(active_cdrs_by_channel);
+
+       if (!match->suspended && active_count != 0) {
+               AST_RWLIST_UNLOCK(&be_list);
+               ast_log(AST_LOG_WARNING, "Unable to unregister CDR backend %s; %d CDRs are still active\n",
+                       name, active_count);
+               return -1;
+       }
+
+       AST_RWLIST_REMOVE(&be_list, match, list);
+       AST_RWLIST_UNLOCK(&be_list);
+
+       ast_verb(2, "Unregistered '%s' CDR backend\n", name);
+       ast_free(match);
+
+       return 0;
+}
+
+struct ast_cdr *ast_cdr_dup(struct ast_cdr *cdr)
+{
+       struct ast_cdr *newcdr;
+
+       if (!cdr) {
+               return NULL;
+       }
+       newcdr = ast_cdr_alloc();
+       if (!newcdr) {
+               return NULL;
        }
+
+       *newcdr = *cdr;
+       AST_LIST_HEAD_INIT_NOLOCK(&newcdr->varshead);
+       copy_variables(&newcdr->varshead, &cdr->varshead);
+       newcdr->next = NULL;
+
+       return newcdr;
 }
 
-struct ast_cdr *ast_cdr_alloc(void)
+static const char *cdr_format_var_internal(struct ast_cdr *cdr, const char *name)
 {
-       struct ast_cdr *x;
-       x = ast_calloc(1, sizeof(*x));
-       if (!x)
-               ast_log(LOG_ERROR,"Allocation Failure for a CDR!\n");
-       return x;
+       struct ast_var_t *variables;
+
+       if (ast_strlen_zero(name)) {
+               return NULL;
+       }
+
+       AST_LIST_TRAVERSE(&cdr->varshead, variables, entries) {
+               if (!strcasecmp(name, ast_var_name(variables))) {
+                       return ast_var_value(variables);
+               }
+       }
+
+       return NULL;
 }
 
-static void cdr_merge_vars(struct ast_cdr *to, struct ast_cdr *from)
+static void cdr_get_tv(struct timeval when, const char *fmt, char *buf, int bufsize)
 {
-       struct ast_var_t *variablesfrom,*variablesto;
-       struct varshead *headpfrom = &to->varshead;
-       struct varshead *headpto = &from->varshead;
-       AST_LIST_TRAVERSE_SAFE_BEGIN(headpfrom, variablesfrom, entries) {
-               /* for every var in from, stick it in to */
-               const char *fromvarname, *fromvarval;
-               const char *tovarname = NULL, *tovarval = NULL;
-               fromvarname = ast_var_name(variablesfrom);
-               fromvarval = ast_var_value(variablesfrom);
-               tovarname = 0;
-
-               /* now, quick see if that var is in the 'to' cdr already */
-               AST_LIST_TRAVERSE(headpto, variablesto, entries) {
+       if (fmt == NULL) {      /* raw mode */
+               snprintf(buf, bufsize, "%ld.%06ld", (long)when.tv_sec, (long)when.tv_usec);
+       } else {
+               buf[0] = '\0';/* Ensure the buffer is initialized. */
+               if (when.tv_sec) {
+                       struct ast_tm tm;
 
-                       /* now, quick see if that var is in the 'to' cdr already */
-                       if ( strcasecmp(fromvarname, ast_var_name(variablesto)) == 0 ) {
-                               tovarname = ast_var_name(variablesto);
-                               tovarval = ast_var_value(variablesto);
-                               break;
-                       }
+                       ast_localtime(&when, &tm, NULL);
+                       ast_strftime(buf, bufsize, fmt, &tm);
                }
-               if (tovarname && strcasecmp(fromvarval,tovarval) != 0) {  /* this message here to see how irritating the userbase finds it */
-                       ast_log(LOG_NOTICE, "Merging CDR's: variable %s value %s dropped in favor of value %s\n", tovarname, fromvarval, tovarval);
-                       continue;
-               } else if (tovarname && strcasecmp(fromvarval,tovarval) == 0) /* if they are the same, the job is done */
-                       continue;
-
-               /* rip this var out of the from cdr, and stick it in the to cdr */
-               AST_LIST_MOVE_CURRENT(headpto, entries);
        }
-       AST_LIST_TRAVERSE_SAFE_END;
 }
 
-void ast_cdr_merge(struct ast_cdr *to, struct ast_cdr *from)
+void ast_cdr_format_var(struct ast_cdr *cdr, const char *name, char **ret, char *workspace, int workspacelen, int raw)
 {
-       struct ast_cdr *zcdr;
-       struct ast_cdr *lto = NULL;
-       struct ast_cdr *lfrom = NULL;
-       int discard_from = 0;
+       const char *fmt = "%Y-%m-%d %T";
+       const char *varbuf;
 
-       if (!to || !from)
+       if (!cdr) {
                return;
-
-       /* don't merge into locked CDR's -- it's bad business */
-       if (ast_test_flag(to, AST_CDR_FLAG_LOCKED)) {
-               zcdr = to; /* safety valve? */
-               while (to->next) {
-                       lto = to;
-                       to = to->next;
-               }
-
-               if (ast_test_flag(to, AST_CDR_FLAG_LOCKED)) {
-                       ast_log(LOG_WARNING, "Merging into locked CDR... no choice.");
-                       to = zcdr; /* safety-- if all there are is locked CDR's, then.... ?? */
-                       lto = NULL;
-               }
        }
 
-       if (ast_test_flag(from, AST_CDR_FLAG_LOCKED)) {
-               struct ast_cdr *llfrom = NULL;
-               discard_from = 1;
-               if (lto) {
-                       /* insert the from stuff after lto */
-                       lto->next = from;
-                       lfrom = from;
-                       while (lfrom && lfrom->next) {
-                               if (!lfrom->next->next)
-                                       llfrom = lfrom;
-                               lfrom = lfrom->next;
-                       }
-                       /* rip off the last entry and put a copy of the to at the end */
-                       llfrom->next = to;
-                       from = lfrom;
-               } else {
-                       /* save copy of the current *to cdr */
-                       struct ast_cdr tcdr;
-                       memcpy(&tcdr, to, sizeof(tcdr));
-                       /* copy in the locked from cdr */
-                       memcpy(to, from, sizeof(*to));
-                       lfrom = from;
-                       while (lfrom && lfrom->next) {
-                               if (!lfrom->next->next)
-                                       llfrom = lfrom;
-                               lfrom = lfrom->next;
-                       }
-                       from->next = NULL;
-                       /* rip off the last entry and put a copy of the to at the end */
-                       if (llfrom == from)
-                               to = to->next = ast_cdr_dup(&tcdr);
-                       else
-                               to = llfrom->next = ast_cdr_dup(&tcdr);
-                       from = lfrom;
-               }
-       }
+       *ret = NULL;
 
-       if (!ast_tvzero(from->start)) {
-               if (!ast_tvzero(to->start)) {
-                       if (ast_tvcmp(to->start, from->start) > 0 ) {
-                               to->start = from->start; /* use the earliest time */
-                               from->start = ast_tv(0,0); /* we actively "steal" these values */
-                       }
-                       /* else nothing to do */
-               } else {
-                       to->start = from->start;
-                       from->start = ast_tv(0,0); /* we actively "steal" these values */
-               }
-       }
-       if (!ast_tvzero(from->answer)) {
-               if (!ast_tvzero(to->answer)) {
-                       if (ast_tvcmp(to->answer, from->answer) > 0 ) {
-                               to->answer = from->answer; /* use the earliest time */
-                               from->answer = ast_tv(0,0); /* we actively "steal" these values */
-                       }
-                       /* we got the earliest answer time, so we'll settle for that? */
+       if (!strcasecmp(name, "clid")) {
+               ast_copy_string(workspace, cdr->clid, workspacelen);
+       } else if (!strcasecmp(name, "src")) {
+               ast_copy_string(workspace, cdr->src, workspacelen);
+       } else if (!strcasecmp(name, "dst")) {
+               ast_copy_string(workspace, cdr->dst, workspacelen);
+       } else if (!strcasecmp(name, "dcontext")) {
+               ast_copy_string(workspace, cdr->dcontext, workspacelen);
+       } else if (!strcasecmp(name, "channel")) {
+               ast_copy_string(workspace, cdr->channel, workspacelen);
+       } else if (!strcasecmp(name, "dstchannel")) {
+               ast_copy_string(workspace, cdr->dstchannel, workspacelen);
+       } else if (!strcasecmp(name, "lastapp")) {
+               ast_copy_string(workspace, cdr->lastapp, workspacelen);
+       } else if (!strcasecmp(name, "lastdata")) {
+               ast_copy_string(workspace, cdr->lastdata, workspacelen);
+       } else if (!strcasecmp(name, "start")) {
+               cdr_get_tv(cdr->start, raw ? NULL : fmt, workspace, workspacelen);
+       } else if (!strcasecmp(name, "answer")) {
+               cdr_get_tv(cdr->answer, raw ? NULL : fmt, workspace, workspacelen);
+       } else if (!strcasecmp(name, "end")) {
+               cdr_get_tv(cdr->end, raw ? NULL : fmt, workspace, workspacelen);
+       } else if (!strcasecmp(name, "duration")) {
+               snprintf(workspace, workspacelen, "%ld", cdr->end.tv_sec != 0 ? cdr->duration : (long)ast_tvdiff_ms(ast_tvnow(), cdr->start) / 1000);
+       } else if (!strcasecmp(name, "billsec")) {
+               snprintf(workspace, workspacelen, "%ld", (cdr->billsec || !ast_tvzero(cdr->end) || ast_tvzero(cdr->answer)) ? cdr->billsec : (long)ast_tvdiff_ms(ast_tvnow(), cdr->answer) / 1000);
+       } else if (!strcasecmp(name, "disposition")) {
+               if (raw) {
+                       snprintf(workspace, workspacelen, "%ld", cdr->disposition);
                } else {
-                       to->answer = from->answer;
-                       from->answer = ast_tv(0,0); /* we actively "steal" these values */
+                       ast_copy_string(workspace, ast_cdr_disp2str(cdr->disposition), workspacelen);
                }
-       }
-       if (!ast_tvzero(from->end)) {
-               if (!ast_tvzero(to->end)) {
-                       if (ast_tvcmp(to->end, from->end) < 0 ) {
-                               to->end = from->end; /* use the latest time */
-                               from->end = ast_tv(0,0); /* we actively "steal" these values */
-                               to->duration = to->end.tv_sec - to->start.tv_sec;  /* don't forget to update the duration, billsec, when we set end */
-                               to->billsec = ast_tvzero(to->answer) ? 0 : to->end.tv_sec - to->answer.tv_sec;
-                       }
-                       /* else, nothing to do */
+       } else if (!strcasecmp(name, "amaflags")) {
+               if (raw) {
+                       snprintf(workspace, workspacelen, "%ld", cdr->amaflags);
                } else {
-                       to->end = from->end;
-                       from->end = ast_tv(0,0); /* we actively "steal" these values */
-                       to->duration = to->end.tv_sec - to->start.tv_sec;
-                       to->billsec = ast_tvzero(to->answer) ? 0 : to->end.tv_sec - to->answer.tv_sec;
+                       ast_copy_string(workspace, ast_channel_amaflags2string(cdr->amaflags), workspacelen);
                }
+       } else if (!strcasecmp(name, "accountcode")) {
+               ast_copy_string(workspace, cdr->accountcode, workspacelen);
+       } else if (!strcasecmp(name, "peeraccount")) {
+               ast_copy_string(workspace, cdr->peeraccount, workspacelen);
+       } else if (!strcasecmp(name, "uniqueid")) {
+               ast_copy_string(workspace, cdr->uniqueid, workspacelen);
+       } else if (!strcasecmp(name, "linkedid")) {
+               ast_copy_string(workspace, cdr->linkedid, workspacelen);
+       } else if (!strcasecmp(name, "userfield")) {
+               ast_copy_string(workspace, cdr->userfield, workspacelen);
+       } else if (!strcasecmp(name, "sequence")) {
+               snprintf(workspace, workspacelen, "%d", cdr->sequence);
+       } else if ((varbuf = cdr_format_var_internal(cdr, name))) {
+               ast_copy_string(workspace, varbuf, workspacelen);
+       } else {
+               workspace[0] = '\0';
        }
-       if (to->disposition < from->disposition) {
-               to->disposition = from->disposition;
-               from->disposition = AST_CDR_NOANSWER;
-       }
-       if (ast_strlen_zero(to->lastapp) && !ast_strlen_zero(from->lastapp)) {
-               ast_copy_string(to->lastapp, from->lastapp, sizeof(to->lastapp));
-               from->lastapp[0] = 0; /* theft */
-       }
-       if (ast_strlen_zero(to->lastdata) && !ast_strlen_zero(from->lastdata)) {
-               ast_copy_string(to->lastdata, from->lastdata, sizeof(to->lastdata));
-               from->lastdata[0] = 0; /* theft */
-       }
-       if (ast_strlen_zero(to->dcontext) && !ast_strlen_zero(from->dcontext)) {
-               ast_copy_string(to->dcontext, from->dcontext, sizeof(to->dcontext));
-               from->dcontext[0] = 0; /* theft */
-       }
-       if (ast_strlen_zero(to->dstchannel) && !ast_strlen_zero(from->dstchannel)) {
-               ast_copy_string(to->dstchannel, from->dstchannel, sizeof(to->dstchannel));
-               from->dstchannel[0] = 0; /* theft */
-       }
-       if (!ast_strlen_zero(from->channel) && (ast_strlen_zero(to->channel) || !strncasecmp(from->channel, "Agent/", 6))) {
-               ast_copy_string(to->channel, from->channel, sizeof(to->channel));
-               from->channel[0] = 0; /* theft */
-       }
-       if (ast_strlen_zero(to->src) && !ast_strlen_zero(from->src)) {
-               ast_copy_string(to->src, from->src, sizeof(to->src));
-               from->src[0] = 0; /* theft */
-       }
-       if (ast_strlen_zero(to->clid) && !ast_strlen_zero(from->clid)) {
-               ast_copy_string(to->clid, from->clid, sizeof(to->clid));
-               from->clid[0] = 0; /* theft */
-       }
-       if (ast_strlen_zero(to->dst) && !ast_strlen_zero(from->dst)) {
-               ast_copy_string(to->dst, from->dst, sizeof(to->dst));
-               from->dst[0] = 0; /* theft */
-       }
-       if (!to->amaflags)
-               to->amaflags = AST_CDR_DOCUMENTATION;
-       if (!from->amaflags)
-               from->amaflags = AST_CDR_DOCUMENTATION; /* make sure both amaflags are set to something (DOC is default) */
-       if (ast_test_flag(from, AST_CDR_FLAG_LOCKED) || (to->amaflags == AST_CDR_DOCUMENTATION && from->amaflags != AST_CDR_DOCUMENTATION)) {
-               to->amaflags = from->amaflags;
-       }
-       if (ast_test_flag(from, AST_CDR_FLAG_LOCKED) || (ast_strlen_zero(to->accountcode) && !ast_strlen_zero(from->accountcode))) {
-               ast_copy_string(to->accountcode, from->accountcode, sizeof(to->accountcode));
-       }
-       if (ast_test_flag(from, AST_CDR_FLAG_LOCKED) || (ast_strlen_zero(to->peeraccount) && !ast_strlen_zero(from->peeraccount))) {
-               ast_copy_string(to->peeraccount, from->peeraccount, sizeof(to->peeraccount));
-       }
-       if (ast_test_flag(from, AST_CDR_FLAG_LOCKED) || (ast_strlen_zero(to->userfield) && !ast_strlen_zero(from->userfield))) {
-               ast_copy_string(to->userfield, from->userfield, sizeof(to->userfield));
-       }
-       /* flags, varsead, ? */
-       cdr_merge_vars(from, to);
-
-       if (ast_test_flag(from, AST_CDR_FLAG_KEEP_VARS))
-               ast_set_flag(to, AST_CDR_FLAG_KEEP_VARS);
-       if (ast_test_flag(from, AST_CDR_FLAG_POSTED))
-               ast_set_flag(to, AST_CDR_FLAG_POSTED);
-       if (ast_test_flag(from, AST_CDR_FLAG_LOCKED))
-               ast_set_flag(to, AST_CDR_FLAG_LOCKED);
-       if (ast_test_flag(from, AST_CDR_FLAG_CHILD))
-               ast_set_flag(to, AST_CDR_FLAG_CHILD);
-       if (ast_test_flag(from, AST_CDR_FLAG_POST_DISABLED))
-               ast_set_flag(to, AST_CDR_FLAG_POST_DISABLED);
 
-       /* last, but not least, we need to merge any forked CDRs to the 'to' cdr */
-       while (from->next) {
-               /* just rip 'em off the 'from' and insert them on the 'to' */
-               zcdr = from->next;
-               from->next = zcdr->next;
-               zcdr->next = NULL;
-               /* zcdr is now ripped from the current list; */
-               ast_cdr_append(to, zcdr);
+       if (!ast_strlen_zero(workspace)) {
+               *ret = workspace;
        }
-       if (discard_from)
-               ast_cdr_discard(from);
 }
 
-void ast_cdr_start(struct ast_cdr *cdr)
+/*
+ * \internal
+ * \brief Callback that finds all CDRs that reference a particular channel by name
+ */
+static int cdr_object_select_all_by_name_cb(void *obj, void *arg, int flags)
 {
-       char *chan;
+       struct cdr_object *cdr = obj;
+       const char *name = arg;
 
-       for (; cdr; cdr = cdr->next) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       chan = S_OR(cdr->channel, "<unknown>");
-                       check_post(cdr);
-                       cdr->start = ast_tvnow();
-               }
+       if (!strcasecmp(cdr->party_a.snapshot->name, name) ||
+                       (cdr->party_b.snapshot && !strcasecmp(cdr->party_b.snapshot->name, name))) {
+               return CMP_MATCH;
        }
+       return 0;
 }
 
-void ast_cdr_answer(struct ast_cdr *cdr)
+/*
+ * \internal
+ * \brief Callback that finds a CDR by channel name
+ */
+static int cdr_object_get_by_name_cb(void *obj, void *arg, int flags)
 {
+       struct cdr_object *cdr = obj;
+       const char *name = arg;
 
-       for (; cdr; cdr = cdr->next) {
-               if (ast_test_flag(cdr, AST_CDR_FLAG_ANSLOCKED))
-                       continue;
-               if (ast_test_flag(cdr, AST_CDR_FLAG_DONT_TOUCH) && ast_test_flag(cdr, AST_CDR_FLAG_LOCKED))
-                       continue;
-               check_post(cdr);
-               if (cdr->disposition < AST_CDR_ANSWERED)
-                       cdr->disposition = AST_CDR_ANSWERED;
-               if (ast_tvzero(cdr->answer))
-                       cdr->answer = ast_tvnow();
+       if (!strcasecmp(cdr->party_a.snapshot->name, name)) {
+               return CMP_MATCH;
        }
+       return 0;
 }
 
-void ast_cdr_busy(struct ast_cdr *cdr)
+/* Read Only CDR variables */
+static const char * const cdr_readonly_vars[] = {
+       "clid",
+       "src",
+       "dst",
+       "dcontext",
+       "channel",
+       "dstchannel",
+       "lastapp",
+       "lastdata",
+       "start",
+       "answer",
+       "end",
+       "duration",
+       "billsec",
+       "disposition",
+       "amaflags",
+       "accountcode",
+       "uniqueid",
+       "linkedid",
+       "userfield",
+       "sequence",
+       NULL
+};
+
+int ast_cdr_setvar(const char *channel_name, const char *name, const char *value)
 {
+       struct cdr_object *cdr;
+       struct cdr_object *it_cdr;
+       struct ao2_iterator *it_cdrs;
+       char *arg = ast_strdupa(channel_name);
+       int x;
 
-       for (; cdr; cdr = cdr->next) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       check_post(cdr);
-                       cdr->disposition = AST_CDR_BUSY;
+       for (x = 0; cdr_readonly_vars[x]; x++) {
+               if (!strcasecmp(name, cdr_readonly_vars[x])) {
+                       ast_log(LOG_ERROR, "Attempt to set the '%s' read-only variable!\n", name);
+                       return -1;
                }
        }
-}
 
-void ast_cdr_failed(struct ast_cdr *cdr)
-{
-       for (; cdr; cdr = cdr->next) {
-               check_post(cdr);
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       check_post(cdr);
-                       if (cdr->disposition < AST_CDR_FAILED)
-                               cdr->disposition = AST_CDR_FAILED;
-               }
+       it_cdrs = ao2_callback(active_cdrs_by_channel, OBJ_MULTIPLE, cdr_object_select_all_by_name_cb, arg);
+       if (!it_cdrs) {
+               ast_log(AST_LOG_ERROR, "Unable to find CDR for channel %s\n", channel_name);
+               return -1;
        }
-}
 
-void ast_cdr_noanswer(struct ast_cdr *cdr)
-{
-       char *chan;
+       for (; (cdr = ao2_iterator_next(it_cdrs)); ao2_unlock(cdr), ao2_cleanup(cdr)) {
+               ao2_lock(cdr);
+               for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+                       struct varshead *headp = NULL;
 
-       while (cdr) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       chan = !ast_strlen_zero(cdr->channel) ? cdr->channel : "<unknown>";
-                       check_post(cdr);
-                       cdr->disposition = AST_CDR_NOANSWER;
+                       if (it_cdr->fn_table == &finalized_state_fn_table) {
+                               continue;
+                       }
+                       if (!strcasecmp(channel_name, it_cdr->party_a.snapshot->name)) {
+                               headp = &it_cdr->party_a.variables;
+                       } else if (it_cdr->party_b.snapshot
+                               && !strcasecmp(channel_name, it_cdr->party_b.snapshot->name)) {
+                               headp = &it_cdr->party_b.variables;
+                       }
+                       if (headp) {
+                               set_variable(headp, name, value);
+                       }
                }
-               cdr = cdr->next;
        }
-}
+       ao2_iterator_destroy(it_cdrs);
 
-/* everywhere ast_cdr_disposition is called, it will call ast_cdr_failed()
-   if ast_cdr_disposition returns a non-zero value */
+       return 0;
+}
 
-int ast_cdr_disposition(struct ast_cdr *cdr, int cause)
+/*!
+ * \brief Format a variable on a \ref cdr_object
+ */
+static void cdr_object_format_var_internal(struct cdr_object *cdr, const char *name, char *value, size_t length)
 {
-       int res = 0;
+       struct ast_var_t *variable;
 
-       for (; cdr; cdr = cdr->next) {
-               switch (cause) {  /* handle all the non failure, busy cases, return 0 not to set disposition,
-                                                       return -1 to set disposition to FAILED */
-               case AST_CAUSE_BUSY:
-                       ast_cdr_busy(cdr);
-                       break;
-               case AST_CAUSE_NO_ANSWER:
-                       ast_cdr_noanswer(cdr);
-                       break;
-               case AST_CAUSE_NORMAL:
-                       break;
-               default:
-                       res = -1;
+       AST_LIST_TRAVERSE(&cdr->party_a.variables, variable, entries) {
+               if (!strcasecmp(name, ast_var_name(variable))) {
+                       ast_copy_string(value, ast_var_value(variable), length);
+                       return;
                }
        }
-       return res;
+
+       *value = '\0';
 }
 
-void ast_cdr_setdestchan(struct ast_cdr *cdr, const char *chann)
+/*!
+ * \brief Format one of the standard properties on a \ref cdr_object
+ */
+static int cdr_object_format_property(struct cdr_object *cdr_obj, const char *name, char *value, size_t length)
 {
-       for (; cdr; cdr = cdr->next) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       check_post(cdr);
-                       ast_copy_string(cdr->dstchannel, chann, sizeof(cdr->dstchannel));
+       struct ast_channel_snapshot *party_a = cdr_obj->party_a.snapshot;
+       struct ast_channel_snapshot *party_b = cdr_obj->party_b.snapshot;
+
+       if (!strcasecmp(name, "clid")) {
+               ast_callerid_merge(value, length, party_a->caller_name, party_a->caller_number, "");
+       } else if (!strcasecmp(name, "src")) {
+               ast_copy_string(value, party_a->caller_number, length);
+       } else if (!strcasecmp(name, "dst")) {
+               ast_copy_string(value, party_a->exten, length);
+       } else if (!strcasecmp(name, "dcontext")) {
+               ast_copy_string(value, party_a->context, length);
+       } else if (!strcasecmp(name, "channel")) {
+               ast_copy_string(value, party_a->name, length);
+       } else if (!strcasecmp(name, "dstchannel")) {
+               if (party_b) {
+                       ast_copy_string(value, party_b->name, length);
+               } else {
+                       ast_copy_string(value, "", length);
                }
+       } else if (!strcasecmp(name, "lastapp")) {
+               ast_copy_string(value, party_a->appl, length);
+       } else if (!strcasecmp(name, "lastdata")) {
+               ast_copy_string(value, party_a->data, length);
+       } else if (!strcasecmp(name, "start")) {
+               cdr_get_tv(cdr_obj->start, NULL, value, length);
+       } else if (!strcasecmp(name, "answer")) {
+               cdr_get_tv(cdr_obj->answer, NULL, value, length);
+       } else if (!strcasecmp(name, "end")) {
+               cdr_get_tv(cdr_obj->end, NULL, value, length);
+       } else if (!strcasecmp(name, "duration")) {
+               snprintf(value, length, "%ld", cdr_object_get_duration(cdr_obj));
+       } else if (!strcasecmp(name, "billsec")) {
+               snprintf(value, length, "%ld", cdr_object_get_billsec(cdr_obj));
+       } else if (!strcasecmp(name, "disposition")) {
+               snprintf(value, length, "%u", cdr_obj->disposition);
+       } else if (!strcasecmp(name, "amaflags")) {
+               snprintf(value, length, "%d", party_a->amaflags);
+       } else if (!strcasecmp(name, "accountcode")) {
+               ast_copy_string(value, party_a->accountcode, length);
+       } else if (!strcasecmp(name, "peeraccount")) {
+               if (party_b) {
+                       ast_copy_string(value, party_b->accountcode, length);
+               } else {
+                       ast_copy_string(value, "", length);
+               }
+       } else if (!strcasecmp(name, "uniqueid")) {
+               ast_copy_string(value, party_a->uniqueid, length);
+       } else if (!strcasecmp(name, "linkedid")) {
+               ast_copy_string(value, cdr_obj->linkedid, length);
+       } else if (!strcasecmp(name, "userfield")) {
+               ast_copy_string(value, cdr_obj->party_a.userfield, length);
+       } else if (!strcasecmp(name, "sequence")) {
+               snprintf(value, length, "%u", cdr_obj->sequence);
+       } else {
+               return 1;
        }
+
+       return 0;
 }
 
-void ast_cdr_setapp(struct ast_cdr *cdr, const char *app, const char *data)
+/*! \internal
+ * \brief Look up and retrieve a CDR object by channel name
+ * \param name The name of the channel
+ * \retval NULL on error
+ * \retval The \ref cdr_object for the channel on success, with the reference
+ *     count bumped by one.
+ */
+static struct cdr_object *cdr_object_get_by_name(const char *name)
 {
+       char *param;
 
-       for (; cdr; cdr = cdr->next) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       check_post(cdr);
-                       ast_copy_string(cdr->lastapp, S_OR(app, ""), sizeof(cdr->lastapp));
-                       ast_copy_string(cdr->lastdata, S_OR(data, ""), sizeof(cdr->lastdata));
-               }
+       if (ast_strlen_zero(name)) {
+               return NULL;
        }
+
+       param = ast_strdupa(name);
+       return ao2_callback(active_cdrs_by_channel, 0, cdr_object_get_by_name_cb, param);
 }
 
-void ast_cdr_setanswer(struct ast_cdr *cdr, struct timeval t)
+int ast_cdr_getvar(const char *channel_name, const char *name, char *value, size_t length)
 {
+       RAII_VAR(struct cdr_object *, cdr, cdr_object_get_by_name(channel_name), ao2_cleanup);
+       struct cdr_object *cdr_obj;
 
-       for (; cdr; cdr = cdr->next) {
-               if (ast_test_flag(cdr, AST_CDR_FLAG_ANSLOCKED))
-                       continue;
-               if (ast_test_flag(cdr, AST_CDR_FLAG_DONT_TOUCH) && ast_test_flag(cdr, AST_CDR_FLAG_LOCKED))
-                       continue;
-               check_post(cdr);
-               cdr->answer = t;
+       if (!cdr) {
+               ast_log(AST_LOG_ERROR, "Unable to find CDR for channel %s\n", channel_name);
+               return 1;
        }
-}
 
-void ast_cdr_setdisposition(struct ast_cdr *cdr, long int disposition)
-{
+       if (ast_strlen_zero(name)) {
+               return 1;
+       }
 
-       for (; cdr; cdr = cdr->next) {
-               if (ast_test_flag(cdr, AST_CDR_FLAG_LOCKED))
-                       continue;
-               check_post(cdr);
-               cdr->disposition = disposition;
+       ao2_lock(cdr);
+
+       cdr_obj = cdr->last;
+       if (cdr_object_format_property(cdr_obj, name, value, length)) {
+               /* Property failed; attempt variable */
+               cdr_object_format_var_internal(cdr_obj, name, value, length);
        }
+
+       ao2_unlock(cdr);
+
+       return 0;
 }
 
-/* set cid info for one record */
-static void set_one_cid(struct ast_cdr *cdr, struct ast_channel *c)
+int ast_cdr_serialize_variables(const char *channel_name, struct ast_str **buf, char delim, char sep)
 {
-       const char *num;
+       RAII_VAR(struct cdr_object *, cdr, cdr_object_get_by_name(channel_name), ao2_cleanup);
+       struct cdr_object *it_cdr;
+       struct ast_var_t *variable;
+       const char *var;
+       RAII_VAR(char *, workspace, ast_malloc(256), ast_free);
+       int total = 0, x = 0, i;
 
-       if (!cdr) {
-               return;
+       if (!workspace) {
+               return 0;
        }
 
-       /* Grab source from ANI or normal Caller*ID */
-       num = S_COR(c->caller.ani.number.valid, c->caller.ani.number.str,
-               S_COR(c->caller.id.number.valid, c->caller.id.number.str, NULL));
-       ast_callerid_merge(cdr->clid, sizeof(cdr->clid),
-               S_COR(c->caller.id.name.valid, c->caller.id.name.str, NULL), num, "");
-       ast_copy_string(cdr->src, S_OR(num, ""), sizeof(cdr->src));
-       ast_cdr_setvar(cdr, "dnid", S_OR(c->dialed.number.str, ""), 0);
+       if (!cdr) {
+               RAII_VAR(struct module_config *, mod_cfg,
+                        ao2_global_obj_ref(module_configs), ao2_cleanup);
 
-       if (c->caller.id.subaddress.valid) {
-               ast_cdr_setvar(cdr, "callingsubaddr", S_OR(c->caller.id.subaddress.str, ""), 0);
-       }
-       if (c->dialed.subaddress.valid) {
-               ast_cdr_setvar(cdr, "calledsubaddr", S_OR(c->dialed.subaddress.str, ""), 0);
-       }
-}
+               if (ast_test_flag(&mod_cfg->general->settings, CDR_ENABLED)) {
+                       ast_log(AST_LOG_ERROR, "Unable to find CDR for channel %s\n", channel_name);
+               }
 
-int ast_cdr_setcid(struct ast_cdr *cdr, struct ast_channel *c)
-{
-       for (; cdr; cdr = cdr->next) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED))
-                       set_one_cid(cdr, c);
+               return 0;
        }
-       return 0;
-}
 
-static int cdr_seq_inc(struct ast_cdr *cdr)
-{
-       return (cdr->sequence = ast_atomic_fetchadd_int(&cdr_sequence, +1));
-}
+       ast_str_reset(*buf);
 
-int ast_cdr_init(struct ast_cdr *cdr, struct ast_channel *c)
-{
-       char *chan;
+       ao2_lock(cdr);
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (++x > 1)
+                       ast_str_append(buf, 0, "\n");
+
+               AST_LIST_TRAVERSE(&it_cdr->party_a.variables, variable, entries) {
+                       if (!(var = ast_var_name(variable))) {
+                               continue;
+                       }
+
+                       if (ast_str_append(buf, 0, "level %d: %s%c%s%c", x, var, delim, S_OR(ast_var_value(variable), ""), sep) < 0) {
+                               ast_log(LOG_ERROR, "Data Buffer Size Exceeded!\n");
+                               break;
+                       }
 
-       for ( ; cdr ; cdr = cdr->next) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       chan = S_OR(cdr->channel, "<unknown>");
-                       ast_copy_string(cdr->channel, c->name, sizeof(cdr->channel));
-                       set_one_cid(cdr, c);
-                       cdr_seq_inc(cdr);
+                       total++;
+               }
+
+               for (i = 0; cdr_readonly_vars[i]; i++) {
+                       if (cdr_object_format_property(it_cdr, cdr_readonly_vars[i], workspace, sizeof(workspace))) {
+                               /* Unhandled read-only CDR variable. */
+                               ast_assert(0);
+                               continue;
+                       }
 
-                       cdr->disposition = (c->_state == AST_STATE_UP) ?  AST_CDR_ANSWERED : AST_CDR_NOANSWER;
-                       cdr->amaflags = c->amaflags ? c->amaflags :  ast_default_amaflags;
-                       ast_copy_string(cdr->accountcode, c->accountcode, sizeof(cdr->accountcode));
-                       ast_copy_string(cdr->peeraccount, c->peeraccount, sizeof(cdr->peeraccount));
-                       /* Destination information */
-                       ast_copy_string(cdr->dst, S_OR(c->macroexten,c->exten), sizeof(cdr->dst));
-                       ast_copy_string(cdr->dcontext, S_OR(c->macrocontext,c->context), sizeof(cdr->dcontext));
-                       /* Unique call identifier */
-                       ast_copy_string(cdr->uniqueid, c->uniqueid, sizeof(cdr->uniqueid));
-                       /* Linked call identifier */
-                       ast_copy_string(cdr->linkedid, c->linkedid, sizeof(cdr->linkedid));
+                       if (!ast_strlen_zero(workspace)
+                               && ast_str_append(buf, 0, "level %d: %s%c%s%c", x, cdr_readonly_vars[i], delim, workspace, sep) < 0) {
+                               ast_log(LOG_ERROR, "Data Buffer Size Exceeded!\n");
+                               break;
+                       }
+                       total++;
                }
        }
-       return 0;
+       ao2_unlock(cdr);
+       return total;
 }
 
-/* Three routines were "fixed" via 10668, and later shown that
-   users were depending on this behavior. ast_cdr_end,
-   ast_cdr_setvar and ast_cdr_answer are the three routines.
-   While most of the other routines would not touch
-   LOCKED cdr's, these three routines were designed to
-   operate on locked CDR's as a matter of course.
-   I now appreciate how this plays with the ForkCDR app,
-   which forms these cdr chains in the first place.
-   cdr_end is pretty key: all cdrs created are closed
-   together. They only vary by start time. Arithmetically,
-   users can calculate the subintervals they wish to track. */
-
-void ast_cdr_end(struct ast_cdr *cdr)
+void ast_cdr_free(struct ast_cdr *cdr)
 {
-       for ( ; cdr ; cdr = cdr->next) {
-               if (ast_test_flag(cdr, AST_CDR_FLAG_DONT_TOUCH) && ast_test_flag(cdr, AST_CDR_FLAG_LOCKED))
-                       continue;
-               check_post(cdr);
-               if (ast_tvzero(cdr->end))
-                       cdr->end = ast_tvnow();
-               if (ast_tvzero(cdr->start)) {
-                       ast_log(LOG_WARNING, "CDR on channel '%s' has not started\n", S_OR(cdr->channel, "<unknown>"));
-                       cdr->disposition = AST_CDR_FAILED;
-               } else
-                       cdr->duration = cdr->end.tv_sec - cdr->start.tv_sec;
-               if (ast_tvzero(cdr->answer)) {
-                       if (cdr->disposition == AST_CDR_ANSWERED) {
-                               ast_log(LOG_WARNING, "CDR on channel '%s' has no answer time but is 'ANSWERED'\n", S_OR(cdr->channel, "<unknown>"));
-                               cdr->disposition = AST_CDR_FAILED;
-                       }
-               } else {
-                       cdr->billsec = cdr->end.tv_sec - cdr->answer.tv_sec;
-                       if (ast_test_flag(&ast_options, AST_OPT_FLAG_INITIATED_SECONDS))
-                               cdr->billsec += cdr->end.tv_usec > cdr->answer.tv_usec ? 1 : 0;
-               }
+       while (cdr) {
+               struct ast_cdr *next = cdr->next;
+
+               free_variables(&cdr->varshead);
+               ast_free(cdr);
+               cdr = next;
        }
 }
 
-char *ast_cdr_disp2str(int disposition)
+struct ast_cdr *ast_cdr_alloc(void)
+{
+       struct ast_cdr *x;
+
+       x = ast_calloc(1, sizeof(*x));
+       return x;
+}
+
+const char *ast_cdr_disp2str(int disposition)
 {
        switch (disposition) {
        case AST_CDR_NULL:
@@ -968,267 +3185,259 @@ char *ast_cdr_disp2str(int disposition)
                return "BUSY";
        case AST_CDR_ANSWERED:
                return "ANSWERED";
+       case AST_CDR_CONGESTION:
+               return "CONGESTION";
        }
        return "UNKNOWN";
 }
 
-/*! Converts AMA flag to printable string */
-char *ast_cdr_flags2str(int flag)
-{
-       switch (flag) {
-       case AST_CDR_OMIT:
-               return "OMIT";
-       case AST_CDR_BILLING:
-               return "BILLING";
-       case AST_CDR_DOCUMENTATION:
-               return "DOCUMENTATION";
-       }
-       return "Unknown";
-}
+struct party_b_userfield_update {
+       const char *channel_name;
+       const char *userfield;
+};
 
-int ast_cdr_setaccount(struct ast_channel *chan, const char *account)
+/*! \brief Callback used to update the userfield on Party B on all CDRs */
+static int cdr_object_update_party_b_userfield_cb(void *obj, void *arg, int flags)
 {
-       struct ast_cdr *cdr = chan->cdr;
-       const char *old_acct = "";
-
-       if (!ast_strlen_zero(chan->accountcode)) {
-               old_acct = ast_strdupa(chan->accountcode);
-       }
-
-       ast_string_field_set(chan, accountcode, account);
-       for ( ; cdr ; cdr = cdr->next) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       ast_copy_string(cdr->accountcode, chan->accountcode, sizeof(cdr->accountcode));
+       struct cdr_object *cdr = obj;
+       struct party_b_userfield_update *info = arg;
+       struct cdr_object *it_cdr;
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (it_cdr->fn_table == &finalized_state_fn_table) {
+                       continue;
                }
-       }
-
-       ast_manager_event(chan, EVENT_FLAG_CALL, "NewAccountCode",
-                       "Channel: %s\r\n"
-                       "Uniqueid: %s\r\n"
-                       "AccountCode: %s\r\n"
-                       "OldAccountCode: %s\r\n",
-                       chan->name, chan->uniqueid, chan->accountcode, old_acct);
-
-       return 0;
-}
-
-int ast_cdr_setpeeraccount(struct ast_channel *chan, const char *account)
-{
-       struct ast_cdr *cdr = chan->cdr;
-       const char *old_acct = "";
-
-       if (!ast_strlen_zero(chan->peeraccount)) {
-               old_acct = ast_strdupa(chan->peeraccount);
-       }
-
-       ast_string_field_set(chan, peeraccount, account);
-       for ( ; cdr ; cdr = cdr->next) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       ast_copy_string(cdr->peeraccount, chan->peeraccount, sizeof(cdr->peeraccount));
+               if (it_cdr->party_b.snapshot
+                       && !strcasecmp(it_cdr->party_b.snapshot->name, info->channel_name)) {
+                       strcpy(it_cdr->party_b.userfield, info->userfield);
                }
        }
-
-       ast_manager_event(chan, EVENT_FLAG_CALL, "NewPeerAccount",
-                       "Channel: %s\r\n"
-                       "Uniqueid: %s\r\n"
-                       "PeerAccount: %s\r\n"
-                       "OldPeerAccount: %s\r\n",
-                       chan->name, chan->uniqueid, chan->peeraccount, old_acct);
-
        return 0;
 }
 
-int ast_cdr_setamaflags(struct ast_channel *chan, const char *flag)
+void ast_cdr_setuserfield(const char *channel_name, const char *userfield)
 {
-       struct ast_cdr *cdr;
-       int newflag = ast_cdr_amaflags2int(flag);
-       if (newflag) {
-               for (cdr = chan->cdr; cdr; cdr = cdr->next) {
-                       if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                               cdr->amaflags = newflag;
+       RAII_VAR(struct cdr_object *, cdr, cdr_object_get_by_name(channel_name), ao2_cleanup);
+       struct party_b_userfield_update party_b_info = {
+                       .channel_name = channel_name,
+                       .userfield = userfield,
+       };
+       struct cdr_object *it_cdr;
+
+       /* Handle Party A */
+       if (cdr) {
+               ao2_lock(cdr);
+               for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+                       if (it_cdr->fn_table == &finalized_state_fn_table) {
+                               continue;
                        }
+                       strcpy(it_cdr->party_a.userfield, userfield);
                }
+               ao2_unlock(cdr);
        }
 
-       return 0;
-}
-
-int ast_cdr_setuserfield(struct ast_channel *chan, const char *userfield)
-{
-       struct ast_cdr *cdr = chan->cdr;
-
-       for ( ; cdr ; cdr = cdr->next) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED))
-                       ast_copy_string(cdr->userfield, userfield, sizeof(cdr->userfield));
-       }
+       /* Handle Party B */
+       ao2_callback(active_cdrs_by_channel, OBJ_NODATA,
+                       cdr_object_update_party_b_userfield_cb,
+                       &party_b_info);
 
-       return 0;
 }
 
-int ast_cdr_appenduserfield(struct ast_channel *chan, const char *userfield)
+static void post_cdr(struct ast_cdr *cdr)
 {
-       struct ast_cdr *cdr = chan->cdr;
-
-       for ( ; cdr ; cdr = cdr->next) {
-               int len = strlen(cdr->userfield);
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       struct cdr_beitem *i;
+
+       for (; cdr ; cdr = cdr->next) {
+               /* For people, who don't want to see unanswered single-channel events */
+               if (!ast_test_flag(&mod_cfg->general->settings, CDR_UNANSWERED) &&
+                               cdr->disposition < AST_CDR_ANSWERED &&
+                               (ast_strlen_zero(cdr->channel) || ast_strlen_zero(cdr->dstchannel))) {
+                       ast_debug(1, "Skipping CDR  for %s since we weren't answered\n", cdr->channel);
+                       continue;
+               }
 
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED))
-                       ast_copy_string(cdr->userfield + len, userfield, sizeof(cdr->userfield) - len);
+               if (ast_test_flag(cdr, AST_CDR_FLAG_DISABLE)) {
+                       continue;
+               }
+               AST_RWLIST_RDLOCK(&be_list);
+               AST_RWLIST_TRAVERSE(&be_list, i, list) {
+                       if (!i->suspended) {
+                               i->be(cdr);
+                       }
+               }
+               AST_RWLIST_UNLOCK(&be_list);
        }
-
-       return 0;
 }
 
-int ast_cdr_update(struct ast_channel *c)
+int ast_cdr_set_property(const char *channel_name, enum ast_cdr_options option)
 {
-       struct ast_cdr *cdr = c->cdr;
-
-       for ( ; cdr ; cdr = cdr->next) {
-               if (!ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       set_one_cid(cdr, c);
+       RAII_VAR(struct cdr_object *, cdr, cdr_object_get_by_name(channel_name), ao2_cleanup);
+       struct cdr_object *it_cdr;
 
-                       /* Copy account code et-al */
-                       ast_copy_string(cdr->accountcode, c->accountcode, sizeof(cdr->accountcode));
-                       ast_copy_string(cdr->peeraccount, c->peeraccount, sizeof(cdr->peeraccount));
-                       ast_copy_string(cdr->linkedid, c->linkedid, sizeof(cdr->linkedid));
+       if (!cdr) {
+               return -1;
+       }
 
-                       /* Destination information */ /* XXX privilege macro* ? */
-                       ast_copy_string(cdr->dst, S_OR(c->macroexten, c->exten), sizeof(cdr->dst));
-                       ast_copy_string(cdr->dcontext, S_OR(c->macrocontext, c->context), sizeof(cdr->dcontext));
+       ao2_lock(cdr);
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (it_cdr->fn_table == &finalized_state_fn_table) {
+                       continue;
                }
+               /* Note: in general, set the flags on both the CDR record as well as the
+                * Party A. Sometimes all we have is the Party A to look at.
+                */
+               ast_set_flag(&it_cdr->flags, option);
+               ast_set_flag(&it_cdr->party_a, option);
        }
+       ao2_unlock(cdr);
 
        return 0;
 }
 
-int ast_cdr_amaflags2int(const char *flag)
+int ast_cdr_clear_property(const char *channel_name, enum ast_cdr_options option)
 {
-       if (!strcasecmp(flag, "default"))
-               return 0;
-       if (!strcasecmp(flag, "omit"))
-               return AST_CDR_OMIT;
-       if (!strcasecmp(flag, "billing"))
-               return AST_CDR_BILLING;
-       if (!strcasecmp(flag, "documentation"))
-               return AST_CDR_DOCUMENTATION;
-       return -1;
-}
-
-static void post_cdr(struct ast_cdr *cdr)
-{
-       char *chan;
-       struct ast_cdr_beitem *i;
-
-       for ( ; cdr ; cdr = cdr->next) {
-               if (!unanswered && cdr->disposition < AST_CDR_ANSWERED && (ast_strlen_zero(cdr->channel) || ast_strlen_zero(cdr->dstchannel))) {
-                       /* For people, who don't want to see unanswered single-channel events */
-                       ast_set_flag(cdr, AST_CDR_FLAG_POST_DISABLED);
-                       continue;
-               }
+       RAII_VAR(struct cdr_object *, cdr, cdr_object_get_by_name(channel_name), ao2_cleanup);
+       struct cdr_object *it_cdr;
 
-               /* don't post CDRs that are for dialed channels unless those
-                * channels were originated from asterisk (pbx_spool, manager,
-                * cli) */
-               if (ast_test_flag(cdr, AST_CDR_FLAG_DIALED) && !ast_test_flag(cdr, AST_CDR_FLAG_ORIGINATED)) {
-                       ast_set_flag(cdr, AST_CDR_FLAG_POST_DISABLED);
-                       continue;
-               }
+       if (!cdr) {
+               return -1;
+       }
 
-               chan = S_OR(cdr->channel, "<unknown>");
-               check_post(cdr);
-               ast_set_flag(cdr, AST_CDR_FLAG_POSTED);
-               if (ast_test_flag(cdr, AST_CDR_FLAG_POST_DISABLED))
-                       continue;
-               AST_RWLIST_RDLOCK(&be_list);
-               AST_RWLIST_TRAVERSE(&be_list, i, list) {
-                       i->be(cdr);
+       ao2_lock(cdr);
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               if (it_cdr->fn_table == &finalized_state_fn_table) {
+                       continue;
                }
-               AST_RWLIST_UNLOCK(&be_list);
+               ast_clear_flag(&it_cdr->flags, option);
        }
+       ao2_unlock(cdr);
+
+       return 0;
 }
 
-void ast_cdr_reset(struct ast_cdr *cdr, struct ast_flags *_flags)
+int ast_cdr_reset(const char *channel_name, int keep_variables)
 {
-       struct ast_cdr *duplicate;
-       struct ast_flags flags = { 0 };
+       RAII_VAR(struct cdr_object *, cdr, cdr_object_get_by_name(channel_name), ao2_cleanup);
+       struct ast_var_t *vardata;
+       struct cdr_object *it_cdr;
 
-       if (_flags)
-               ast_copy_flags(&flags, _flags, AST_FLAGS_ALL);
-
-       for ( ; cdr ; cdr = cdr->next) {
-               /* Detach if post is requested */
-               if (ast_test_flag(&flags, AST_CDR_FLAG_LOCKED) || !ast_test_flag(cdr, AST_CDR_FLAG_LOCKED)) {
-                       if (ast_test_flag(&flags, AST_CDR_FLAG_POSTED)) {
-                               ast_cdr_end(cdr);
-                               if ((duplicate = ast_cdr_dup_unique_swap(cdr))) {
-                                       ast_cdr_detach(duplicate);
-                               }
-                               ast_set_flag(cdr, AST_CDR_FLAG_POSTED);
-                       }
+       if (!cdr) {
+               return -1;
+       }
 
-                       /* enable CDR only */
-                       if (ast_test_flag(&flags, AST_CDR_FLAG_POST_ENABLE)) {
-                               ast_clear_flag(cdr, AST_CDR_FLAG_POST_DISABLED);
-                               continue;
+       ao2_lock(cdr);
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               /* clear variables */
+               if (!keep_variables) {
+                       while ((vardata = AST_LIST_REMOVE_HEAD(&it_cdr->party_a.variables, entries))) {
+                               ast_var_delete(vardata);
                        }
-
-                       /* clear variables */
-                       if (!ast_test_flag(&flags, AST_CDR_FLAG_KEEP_VARS)) {
-                               ast_cdr_free_vars(cdr, 0);
+                       if (cdr->party_b.snapshot) {
+                               while ((vardata = AST_LIST_REMOVE_HEAD(&it_cdr->party_b.variables, entries))) {
+                                       ast_var_delete(vardata);
+                               }
                        }
-
-                       /* Reset to initial state */
-                       ast_clear_flag(cdr, AST_FLAGS_ALL);
-                       memset(&cdr->start, 0, sizeof(cdr->start));
-                       memset(&cdr->end, 0, sizeof(cdr->end));
-                       memset(&cdr->answer, 0, sizeof(cdr->answer));
-                       cdr->billsec = 0;
-                       cdr->duration = 0;
-                       ast_cdr_start(cdr);
-                       cdr->disposition = AST_CDR_NOANSWER;
                }
+
+               /* Reset to initial state */
+               memset(&it_cdr->start, 0, sizeof(it_cdr->start));
+               memset(&it_cdr->end, 0, sizeof(it_cdr->end));
+               memset(&it_cdr->answer, 0, sizeof(it_cdr->answer));
+               it_cdr->start = ast_tvnow();
+               cdr_object_check_party_a_answer(it_cdr);
        }
+       ao2_unlock(cdr);
+
+       return 0;
 }
 
-void ast_cdr_specialized_reset(struct ast_cdr *cdr, struct ast_flags *_flags)
+int ast_cdr_fork(const char *channel_name, struct ast_flags *options)
 {
-       struct ast_flags flags = { 0 };
-
-       if (_flags)
-               ast_copy_flags(&flags, _flags, AST_FLAGS_ALL);
+       RAII_VAR(struct cdr_object *, cdr, cdr_object_get_by_name(channel_name), ao2_cleanup);
+       struct cdr_object *new_cdr;
+       struct cdr_object *it_cdr;
+       struct cdr_object *cdr_obj;
 
-       /* Reset to initial state */
-       if (ast_test_flag(cdr, AST_CDR_FLAG_POST_DISABLED)) { /* But do NOT lose the NoCDR() setting */
-               ast_clear_flag(cdr, AST_FLAGS_ALL);
-               ast_set_flag(cdr, AST_CDR_FLAG_POST_DISABLED);
-       } else {
-               ast_clear_flag(cdr, AST_FLAGS_ALL);
+       if (!cdr) {
+               return -1;
        }
 
-       memset(&cdr->start, 0, sizeof(cdr->start));
-       memset(&cdr->end, 0, sizeof(cdr->end));
-       memset(&cdr->answer, 0, sizeof(cdr->answer));
-       cdr->billsec = 0;
-       cdr->duration = 0;
-       ast_cdr_start(cdr);
-       cdr->disposition = AST_CDR_NULL;
-}
+       {
+               SCOPED_AO2LOCK(lock, cdr);
 
-struct ast_cdr *ast_cdr_append(struct ast_cdr *cdr, struct ast_cdr *newcdr)
-{
-       struct ast_cdr *ret;
+               cdr_obj = cdr->last;
+               if (cdr_obj->fn_table == &finalized_state_fn_table) {
+                       /* If the last CDR in the chain is finalized, don't allow a fork -
+                        * things are already dying at this point
+                        */
+                       return -1;
+               }
 
-       if (cdr) {
-               ret = cdr;
+               /* Copy over the basic CDR information. The Party A information is
+                * copied over automatically as part of the append
+                */
+               ast_debug(1, "Forking CDR for channel %s\n", cdr->party_a.snapshot->name);
+               new_cdr = cdr_object_create_and_append(cdr);
+               if (!new_cdr) {
+                       return -1;
+               }
+               new_cdr->fn_table = cdr_obj->fn_table;
+               ast_string_field_set(new_cdr, bridge, cdr->bridge);
+               ast_string_field_set(new_cdr, appl, cdr->appl);
+               ast_string_field_set(new_cdr, data, cdr->data);
+               ast_string_field_set(new_cdr, context, cdr->context);
+               ast_string_field_set(new_cdr, exten, cdr->exten);
+               new_cdr->flags = cdr->flags;
+               /* Explicitly clear the AST_CDR_LOCK_APP flag - we want
+                * the application to be changed on the new CDR if the
+                * dialplan demands it
+                */
+               ast_clear_flag(&new_cdr->flags, AST_CDR_LOCK_APP);
+
+               /* If there's a Party B, copy it over as well */
+               if (cdr_obj->party_b.snapshot) {
+                       new_cdr->party_b.snapshot = cdr_obj->party_b.snapshot;
+                       ao2_ref(new_cdr->party_b.snapshot, +1);
+                       strcpy(new_cdr->party_b.userfield, cdr_obj->party_b.userfield);
+                       new_cdr->party_b.flags = cdr_obj->party_b.flags;
+                       if (ast_test_flag(options, AST_CDR_FLAG_KEEP_VARS)) {
+                               copy_variables(&new_cdr->party_b.variables, &cdr_obj->party_b.variables);
+                       }
+               }
+               new_cdr->start = cdr_obj->start;
+               new_cdr->answer = cdr_obj->answer;
 
-               while (cdr->next)
-                       cdr = cdr->next;
-               cdr->next = newcdr;
-       } else {
-               ret = newcdr;
+               /* Modify the times based on the flags passed in */
+               if (ast_test_flag(options, AST_CDR_FLAG_SET_ANSWER)
+                               && new_cdr->party_a.snapshot->state == AST_STATE_UP) {
+                       new_cdr->answer = ast_tvnow();
+               }
+               if (ast_test_flag(options, AST_CDR_FLAG_RESET)) {
+                       new_cdr->answer = ast_tvnow();
+                       new_cdr->start = ast_tvnow();
+               }
+
+               /* Create and append, by default, copies over the variables */
+               if (!ast_test_flag(options, AST_CDR_FLAG_KEEP_VARS)) {
+                       free_variables(&new_cdr->party_a.variables);
+               }
+
+               /* Finalize any current CDRs */
+               if (ast_test_flag(options, AST_CDR_FLAG_FINALIZE)) {
+                       for (it_cdr = cdr; it_cdr != new_cdr; it_cdr = it_cdr->next) {
+                               if (it_cdr->fn_table == &finalized_state_fn_table) {
+                                       continue;
+                               }
+                               /* Force finalization on the CDR. This will bypass any checks for
+                                * end before 'h' extension.
+                                */
+                               cdr_object_finalize(it_cdr);
+                               cdr_object_transition_state(it_cdr, &finalized_state_fn_table);
+                       }
+               }
        }
 
-       return ret;
+       return 0;
 }
 
 /*! \note Don't call without cdr_batch_lock */
@@ -1253,8 +3462,8 @@ static int init_batch(void)
 
 static void *do_batch_backend_process(void *data)
 {
-       struct ast_cdr_batch_item *processeditem;
-       struct ast_cdr_batch_item *batchitem = data;
+       struct cdr_batch_item *processeditem;
+       struct cdr_batch_item *batchitem = data;
 
        /* Push each CDR into storage mechanism(s) and free all the memory */
        while (batchitem) {
@@ -1268,14 +3477,16 @@ static void *do_batch_backend_process(void *data)
        return NULL;
 }
 
-void ast_cdr_submit_batch(int do_shutdown)
+static void cdr_submit_batch(int do_shutdown)
 {
-       struct ast_cdr_batch_item *oldbatchitems = NULL;
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       struct cdr_batch_item *oldbatchitems = NULL;
        pthread_t batch_post_thread = AST_PTHREADT_NULL;
 
        /* if there's no batch, or no CDRs in the batch, then there's nothing to do */
-       if (!batch || !batch->head)
+       if (!batch || !batch->head) {
                return;
+       }
 
        /* move the old CDRs aside, and prepare a new CDR batch */
        ast_mutex_lock(&cdr_batch_lock);
@@ -1285,7 +3496,7 @@ void ast_cdr_submit_batch(int do_shutdown)
 
        /* if configured, spawn a new thread to post these CDRs,
           also try to save as much as possible if we are shutting down safely */
-       if (batchscheduleronly || do_shutdown) {
+       if (ast_test_flag(&mod_cfg->general->batch_settings.settings, BATCH_MODE_SCHEDULER_ONLY) || do_shutdown) {
                ast_debug(1, "CDR single-threaded batch processing begins now\n");
                do_batch_backend_process(oldbatchitems);
        } else {
@@ -1300,43 +3511,54 @@ void ast_cdr_submit_batch(int do_shutdown)
 
 static int submit_scheduled_batch(const void *data)
 {
-       ast_cdr_submit_batch(0);
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       cdr_submit_batch(0);
        /* manually reschedule from this point in time */
-       cdr_sched = ast_sched_add(sched, batchtime * 1000, submit_scheduled_batch, NULL);
+
+       ast_mutex_lock(&cdr_sched_lock);
+       cdr_sched = ast_sched_add(sched, mod_cfg->general->batch_settings.time * 1000, submit_scheduled_batch, NULL);
+       ast_mutex_unlock(&cdr_sched_lock);
        /* returning zero so the scheduler does not automatically reschedule */
        return 0;
 }
 
+/*! Do not hold the batch lock while calling this function */
 static void submit_unscheduled_batch(void)
 {
+       /* Prevent two deletes from happening at the same time */
+       ast_mutex_lock(&cdr_sched_lock);
        /* this is okay since we are not being called from within the scheduler */
        AST_SCHED_DEL(sched, cdr_sched);
        /* schedule the submission to occur ASAP (1 ms) */
        cdr_sched = ast_sched_add(sched, 1, submit_scheduled_batch, NULL);
+       ast_mutex_unlock(&cdr_sched_lock);
+
        /* signal the do_cdr thread to wakeup early and do some work (that lazy thread ;) */
        ast_mutex_lock(&cdr_pending_lock);
        ast_cond_signal(&cdr_pending_cond);
        ast_mutex_unlock(&cdr_pending_lock);
 }
 
-void ast_cdr_detach(struct ast_cdr *cdr)
+static void cdr_detach(struct ast_cdr *cdr)
 {
-       struct ast_cdr_batch_item *newtail;
+       struct cdr_batch_item *newtail;
        int curr;
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       int submit_batch = 0;
 
-       if (!cdr)
+       if (!cdr) {
                return;
+       }
 
        /* maybe they disabled CDR stuff completely, so just drop it */
-       if (!enabled) {
+       if (!ast_test_flag(&mod_cfg->general->settings, CDR_ENABLED)) {
                ast_debug(1, "Dropping CDR !\n");
-               ast_set_flag(cdr, AST_CDR_FLAG_POST_DISABLED);
                ast_cdr_free(cdr);
                return;
        }
 
        /* post stuff immediately if we are not in batch mode, this is legacy behaviour */
-       if (!batchmode) {
+       if (!ast_test_flag(&mod_cfg->general->settings, CDR_BATCHMODE)) {
                post_cdr(cdr);
                ast_cdr_free(cdr);
                return;
@@ -1366,11 +3588,17 @@ void ast_cdr_detach(struct ast_cdr *cdr)
        newtail->cdr = cdr;
        batch->tail = newtail;
        curr = batch->size++;
-       ast_mutex_unlock(&cdr_batch_lock);
 
        /* if we have enough stuff to post, then do it */
-       if (curr >= (batchsize - 1))
+       if (curr >= (mod_cfg->general->batch_settings.size - 1)) {
+               submit_batch = 1;
+       }
+       ast_mutex_unlock(&cdr_batch_lock);
+
+       /* Don't call submit_unscheduled_batch with the cdr_batch_lock held */
+       if (submit_batch) {
                submit_unscheduled_batch();
+       }
 }
 
 static void *do_cdr(void *data)
@@ -1399,11 +3627,220 @@ static void *do_cdr(void *data)
        return NULL;
 }
 
+static char *handle_cli_debug(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "cdr set debug [on|off]";
+               e->usage = "Enable or disable extra debugging in the CDR Engine. Note\n"
+                               "that this will dump debug information to the VERBOSE setting\n"
+                               "and should only be used when debugging information from the\n"
+                               "CDR engine is needed.\n";
+               return NULL;
+       case CLI_GENERATE:
+               return NULL;
+       }
+
+       if (a->argc != 4) {
+               return CLI_SHOWUSAGE;
+       }
+
+       if (!strcasecmp(a->argv[3], "on")
+               && !ast_test_flag(&mod_cfg->general->settings, CDR_DEBUG)) {
+               ast_set_flag(&mod_cfg->general->settings, CDR_DEBUG);
+               ast_cli(a->fd, "CDR debugging enabled\n");
+       } else if (!strcasecmp(a->argv[3], "off")
+               && ast_test_flag(&mod_cfg->general->settings, CDR_DEBUG)) {
+               ast_clear_flag(&mod_cfg->general->settings, CDR_DEBUG);
+               ast_cli(a->fd, "CDR debugging disabled\n");
+       }
+
+       return CLI_SUCCESS;
+}
+
+/*! \brief Complete user input for 'cdr show' */
+static char *cli_complete_show(struct ast_cli_args *a)
+{
+       char *result = NULL;
+       int wordlen = strlen(a->word);
+       int which = 0;
+       struct ao2_iterator it_cdrs;
+       struct cdr_object *cdr;
+
+       it_cdrs = ao2_iterator_init(active_cdrs_by_channel, 0);
+       while ((cdr = ao2_iterator_next(&it_cdrs))) {
+               if (!strncasecmp(a->word, cdr->party_a.snapshot->name, wordlen) &&
+                       (++which > a->n)) {
+                       result = ast_strdup(cdr->party_a.snapshot->name);
+                       if (result) {
+                               ao2_ref(cdr, -1);
+                               break;
+                       }
+               }
+               ao2_ref(cdr, -1);
+       }
+       ao2_iterator_destroy(&it_cdrs);
+       return result;
+}
+
+static void cli_show_channels(struct ast_cli_args *a)
+{
+       struct ao2_iterator it_cdrs;
+       struct cdr_object *cdr;
+       char start_time_buffer[64];
+       char answer_time_buffer[64];
+       char end_time_buffer[64];
+
+#define TITLE_STRING "%-25.25s %-25.25s %-15.15s %-8.8s %-8.8s %-8.8s %-8.8s %-8.8s\n"
+#define FORMAT_STRING "%-25.25s %-25.25s %-15.15s %-8.8s %-8.8s %-8.8s %-8.8ld %-8.8ld\n"
+
+       ast_cli(a->fd, "\n");
+       ast_cli(a->fd, "Channels with Call Detail Record (CDR) Information\n");
+       ast_cli(a->fd, "--------------------------------------------------\n");
+       ast_cli(a->fd, TITLE_STRING, "Channel", "Dst. Channel", "LastApp", "Start", "Answer", "End", "Billsec", "Duration");
+
+       it_cdrs = ao2_iterator_init(active_cdrs_by_channel, 0);
+       for (; (cdr = ao2_iterator_next(&it_cdrs)); ao2_cleanup(cdr)) {
+               struct cdr_object *it_cdr;
+               struct timeval start_time = { 0, };
+               struct timeval answer_time = { 0, };
+               struct timeval end_time = { 0, };
+
+               SCOPED_AO2LOCK(lock, cdr);
+
+               /* Calculate the start, end, answer, billsec, and duration over the
+                * life of all of the CDR entries
+                */
+               for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+                       if (snapshot_is_dialed(it_cdr->party_a.snapshot)) {
+                               continue;
+                       }
+                       if (ast_tvzero(start_time)) {
+                               start_time = it_cdr->start;
+                       }
+                       if (!ast_tvzero(it_cdr->answer) && ast_tvzero(answer_time)) {
+                               answer_time = it_cdr->answer;
+                       }
+               }
+
+               /* If there was no start time, then all CDRs were for a dialed channel; skip */
+               if (ast_tvzero(start_time)) {
+                       continue;
+               }
+               it_cdr = cdr->last;
+
+               end_time = ast_tvzero(it_cdr->end) ? ast_tvnow() : it_cdr->end;
+               cdr_get_tv(start_time, "%T", start_time_buffer, sizeof(start_time_buffer));
+               cdr_get_tv(answer_time, "%T", answer_time_buffer, sizeof(answer_time_buffer));
+               cdr_get_tv(end_time, "%T", end_time_buffer, sizeof(end_time_buffer));
+               ast_cli(a->fd, FORMAT_STRING, it_cdr->party_a.snapshot->name,
+                               it_cdr->party_b.snapshot ? it_cdr->party_b.snapshot->name : "<none>",
+                               it_cdr->appl,
+                               start_time_buffer,
+                               answer_time_buffer,
+                               end_time_buffer,
+                               ast_tvzero(answer_time) ? 0 : (long)ast_tvdiff_ms(end_time, answer_time) / 1000,
+                               (long)ast_tvdiff_ms(end_time, start_time) / 1000);
+       }
+       ao2_iterator_destroy(&it_cdrs);
+#undef FORMAT_STRING
+#undef TITLE_STRING
+}
+
+static void cli_show_channel(struct ast_cli_args *a)
+{
+       struct cdr_object *it_cdr;
+       char clid[64];
+       char start_time_buffer[64];
+       char answer_time_buffer[64];
+       char end_time_buffer[64];
+       const char *channel_name = a->argv[3];
+       RAII_VAR(struct cdr_object *, cdr, NULL, ao2_cleanup);
+
+#define TITLE_STRING "%-10.10s %-20.20s %-25.25s %-15.15s %-15.15s %-8.8s %-8.8s %-8.8s %-8.8s %-8.8s\n"
+#define FORMAT_STRING "%-10.10s %-20.20s %-25.25s %-15.15s %-15.15s %-8.8s %-8.8s %-8.8s %-8.8ld %-8.8ld\n"
+
+       cdr = cdr_object_get_by_name(channel_name);
+       if (!cdr) {
+               ast_cli(a->fd, "Unknown channel: %s\n", channel_name);
+               return;
+       }
+
+       ast_cli(a->fd, "\n");
+       ast_cli(a->fd, "Call Detail Record (CDR) Information for %s\n", channel_name);
+       ast_cli(a->fd, "--------------------------------------------------\n");
+       ast_cli(a->fd, TITLE_STRING, "AccountCode", "CallerID", "Dst. Channel", "LastApp", "Data", "Start", "Answer", "End", "Billsec", "Duration");
+
+       ao2_lock(cdr);
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               struct timeval end;
+               if (snapshot_is_dialed(it_cdr->party_a.snapshot)) {
+                       continue;
+               }
+               ast_callerid_merge(clid, sizeof(clid), it_cdr->party_a.snapshot->caller_name, it_cdr->party_a.snapshot->caller_number, "");
+               if (ast_tvzero(it_cdr->end)) {
+                       end = ast_tvnow();
+               } else {
+                       end = it_cdr->end;
+               }
+               cdr_get_tv(it_cdr->start, "%T", start_time_buffer, sizeof(start_time_buffer));
+               cdr_get_tv(it_cdr->answer, "%T", answer_time_buffer, sizeof(answer_time_buffer));
+               cdr_get_tv(end, "%T", end_time_buffer, sizeof(end_time_buffer));
+               ast_cli(a->fd, FORMAT_STRING,
+                               it_cdr->party_a.snapshot->accountcode,
+                               clid,
+                               it_cdr->party_b.snapshot ? it_cdr->party_b.snapshot->name : "<none>",
+                               it_cdr->appl,
+                               it_cdr->data,
+                               start_time_buffer,
+                               answer_time_buffer,
+                               end_time_buffer,
+                               (long)ast_tvdiff_ms(end, it_cdr->answer) / 1000,
+                               (long)ast_tvdiff_ms(end, it_cdr->start) / 1000);
+       }
+       ao2_unlock(cdr);
+#undef FORMAT_STRING
+#undef TITLE_STRING
+}
+
+static char *handle_cli_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+       switch (cmd) {
+       case CLI_INIT:
+                       e->command = "cdr show active";
+                       e->usage =
+                               "Usage: cdr show active [channel]\n"
+                               "       Displays a summary of all Call Detail Records when [channel]\n"
+                               "       is omitted; displays all of the Call Detail Records\n"
+                               "       currently in flight for a given [channel] when [channel] is\n"
+                               "       specified.\n\n"
+                               "       Note that this will not display Call Detail Records that\n"
+                               "       have already been dispatched to a backend storage, nor for\n"
+                               "       channels that are no longer active.\n";
+                       return NULL;
+       case CLI_GENERATE:
+               return cli_complete_show(a);
+       }
+
+       if (a->argc > 4) {
+               return CLI_SHOWUSAGE;
+       } else if (a->argc < 4) {
+               cli_show_channels(a);
+       } else {
+               cli_show_channel(a);
+       }
+
+       return CLI_SUCCESS;
+}
+
 static char *handle_cli_status(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
 {
-       struct ast_cdr_beitem *beitem=NULL;
-       int cnt=0;
-       long nextbatchtime=0;
+       struct cdr_beitem *beitem = NULL;
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       int cnt = 0;
+       long nextbatchtime = 0;
 
        switch (cmd) {
        case CLI_INIT:
@@ -1416,28 +3853,30 @@ static char *handle_cli_status(struct ast_cli_entry *e, int cmd, struct ast_cli_
                return NULL;
        }
 
-       if (a->argc > 3)
+       if (a->argc > 3) {
                return CLI_SHOWUSAGE;
+       }
 
        ast_cli(a->fd, "\n");
        ast_cli(a->fd, "Call Detail Record (CDR) settings\n");
        ast_cli(a->fd, "----------------------------------\n");
-       ast_cli(a->fd, "  Logging:                    %s\n", enabled ? "Enabled" : "Disabled");
-       ast_cli(a->fd, "  Mode:                       %s\n", batchmode ? "Batch" : "Simple");
-       if (enabled) {
-               ast_cli(a->fd, "  Log unanswered calls:       %s\n\n", unanswered ? "Yes" : "No");
-               if (batchmode) {
+       ast_cli(a->fd, "  Logging:                    %s\n", ast_test_flag(&mod_cfg->general->settings, CDR_ENABLED) ? "Enabled" : "Disabled");
+       ast_cli(a->fd, "  Mode:                       %s\n", ast_test_flag(&mod_cfg->general->settings, CDR_BATCHMODE) ? "Batch" : "Simple");
+       if (ast_test_flag(&mod_cfg->general->settings, CDR_ENABLED)) {
+               ast_cli(a->fd, "  Log unanswered calls:       %s\n", ast_test_flag(&mod_cfg->general->settings, CDR_UNANSWERED) ? "Yes" : "No");
+               ast_cli(a->fd, "  Log congestion:             %s\n\n", ast_test_flag(&mod_cfg->general->settings, CDR_CONGESTION) ? "Yes" : "No");
+               if (ast_test_flag(&mod_cfg->general->settings, CDR_BATCHMODE)) {
                        ast_cli(a->fd, "* Batch Mode Settings\n");
                        ast_cli(a->fd, "  -------------------\n");
                        if (batch)
                                cnt = batch->size;
                        if (cdr_sched > -1)
                                nextbatchtime = ast_sched_when(sched, cdr_sched);
-                       ast_cli(a->fd, "  Safe shutdown:              %s\n", batchsafeshutdown ? "Enabled" : "Disabled");
-                       ast_cli(a->fd, "  Threading model:            %s\n", batchscheduleronly ? "Scheduler only" : "Scheduler plus separate threads");
+                       ast_cli(a->fd, "  Safe shutdown:              %s\n", ast_test_flag(&mod_cfg->general->batch_settings.settings, BATCH_MODE_SAFE_SHUTDOWN) ? "Enabled" : "Disabled");
+                       ast_cli(a->fd, "  Threading model:            %s\n", ast_test_flag(&mod_cfg->general->batch_settings.settings, BATCH_MODE_SCHEDULER_ONLY) ? "Scheduler only" : "Scheduler plus separate threads");
                        ast_cli(a->fd, "  Current batch size:         %d record%s\n", cnt, ESS(cnt));
-                       ast_cli(a->fd, "  Maximum batch size:         %d record%s\n", batchsize, ESS(batchsize));
-                       ast_cli(a->fd, "  Maximum batch time:         %d second%s\n", batchtime, ESS(batchtime));
+                       ast_cli(a->fd, "  Maximum batch size:         %u record%s\n", mod_cfg->general->batch_settings.size, ESS(mod_cfg->general->batch_settings.size));
+                       ast_cli(a->fd, "  Maximum batch time:         %u second%s\n", mod_cfg->general->batch_settings.time, ESS(mod_cfg->general->batch_settings.time));
                        ast_cli(a->fd, "  Next batch processing time: %ld second%s\n\n", nextbatchtime, ESS(nextbatchtime));
                }
                ast_cli(a->fd, "* Registered Backends\n");
@@ -1447,7 +3886,7 @@ static char *handle_cli_status(struct ast_cli_entry *e, int cmd, struct ast_cli_
                        ast_cli(a->fd, "    (none)\n");
                } else {
                        AST_RWLIST_TRAVERSE(&be_list, beitem, list) {
-                               ast_cli(a->fd, "    %s\n", beitem->name);
+                               ast_cli(a->fd, "    %s%s\n", beitem->name, beitem->suspended ? " (suspended) " : "");
                        }
                }
                AST_RWLIST_UNLOCK(&be_list);
@@ -1459,18 +3898,32 @@ static char *handle_cli_status(struct ast_cli_entry *e, int cmd, struct ast_cli_
 
 static char *handle_cli_submit(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
 {
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+
        switch (cmd) {
        case CLI_INIT:
                e->command = "cdr submit";
                e->usage =
                        "Usage: cdr submit\n"
-                       "       Posts all pending batched CDR data to the configured CDR backend engine modules.\n";
+                       "Posts all pending batched CDR data to the configured CDR\n"
+                       "backend engine modules.\n";
                return NULL;
        case CLI_GENERATE:
                return NULL;
        }
-       if (a->argc > 2)
+       if (a->argc > 2) {
                return CLI_SHOWUSAGE;
+       }
+
+       if (!ast_test_flag(&mod_cfg->general->settings, CDR_ENABLED)) {
+               ast_cli(a->fd, "Cannot submit CDR batch: CDR engine disabled.\n");
+               return CLI_SUCCESS;
+       }
+
+       if (ast_test_flag(&mod_cfg->general->settings, CDR_BATCHMODE)) {
+               ast_cli(a->fd, "Cannot submit CDR batch: batch mode not enabled.\n");
+               return CLI_SUCCESS;
+       }
 
        submit_unscheduled_batch();
        ast_cli(a->fd, "Submitted CDRs to backend engines for processing.  This may take a while.\n");
@@ -1478,143 +3931,271 @@ static char *handle_cli_submit(struct ast_cli_entry *e, int cmd, struct ast_cli_
        return CLI_SUCCESS;
 }
 
-static struct ast_cli_entry cli_submit = AST_CLI_DEFINE(handle_cli_submit, "Posts all pending batched CDR data");
-static struct ast_cli_entry cli_status = AST_CLI_DEFINE(handle_cli_status, "Display the CDR status");
-
-static int do_reload(int reload)
-{
-       struct ast_config *config;
-       const char *enabled_value;
-       const char *unanswered_value;
-       const char *batched_value;
-       const char *scheduleronly_value;
-       const char *batchsafeshutdown_value;
-       const char *size_value;
-       const char *time_value;
-       const char *end_before_h_value;
-       const char *initiatedseconds_value;
-       int cfg_size;
-       int cfg_time;
-       int was_enabled;
-       int was_batchmode;
-       int res=0;
-       struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
-
-       if ((config = ast_config_load2("cdr.conf", "cdr", config_flags)) == CONFIG_STATUS_FILEUNCHANGED) {
-               return 0;
+static struct ast_cli_entry cli_commands[] = {
+       AST_CLI_DEFINE(handle_cli_submit, "Posts all pending batched CDR data"),
+       AST_CLI_DEFINE(handle_cli_status, "Display the CDR status"),
+       AST_CLI_DEFINE(handle_cli_show, "Display active CDRs for channels"),
+       AST_CLI_DEFINE(handle_cli_debug, "Enable debugging in the CDR engine"),
+};
+
+/*!
+ * \brief This dispatches *all* \ref cdr_objects. It should only be used during
+ * shutdown, so that we get billing records for everything that we can.
+ */
+static int cdr_object_dispatch_all_cb(void *obj, void *arg, int flags)
+{
+       struct cdr_object *cdr = obj;
+       struct cdr_object *it_cdr;
+
+       ao2_lock(cdr);
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               cdr_object_transition_state(it_cdr, &finalized_state_fn_table);
        }
+       cdr_object_dispatch(cdr);
+       ao2_unlock(cdr);
 
-       ast_mutex_lock(&cdr_batch_lock);
+       return 0;
+}
+
+static void finalize_batch_mode(void)
+{
+       if (cdr_thread == AST_PTHREADT_NULL) {
+               return;
+       }
+       /* wake up the thread so it will exit */
+       pthread_cancel(cdr_thread);
+       pthread_kill(cdr_thread, SIGURG);
+       pthread_join(cdr_thread, NULL);
+       cdr_thread = AST_PTHREADT_NULL;
+       ast_cond_destroy(&cdr_pending_cond);
+       ast_cdr_engine_term();
+}
+
+struct stasis_message_router *ast_cdr_message_router(void)
+{
+       if (!stasis_router) {
+               return NULL;
+       }
+
+       ao2_bump(stasis_router);
+       return stasis_router;
+}
 
-       was_enabled = enabled;
-       was_batchmode = batchmode;
+/*!
+ * \brief Destroy the active Stasis subscriptions
+ */
+static void destroy_subscriptions(void)
+{
+       channel_subscription = stasis_forward_cancel(channel_subscription);
+       bridge_subscription = stasis_forward_cancel(bridge_subscription);
+       parking_subscription = stasis_forward_cancel(parking_subscription);
+}
 
-       batchsize = BATCH_SIZE_DEFAULT;
-       batchtime = BATCH_TIME_DEFAULT;
-       batchscheduleronly = BATCH_SCHEDULER_ONLY_DEFAULT;
-       batchsafeshutdown = BATCH_SAFE_SHUTDOWN_DEFAULT;
-       enabled = ENABLED_DEFAULT;
-       batchmode = BATCHMODE_DEFAULT;
-       unanswered = UNANSWERED_DEFAULT;
+/*!
+ * \brief Create the Stasis subcriptions for CDRs
+ */
+static int create_subscriptions(void)
+{
+       if (!cdr_topic) {
+               return -1;
+       }
 
-       if (config == CONFIG_STATUS_FILEMISSING || config == CONFIG_STATUS_FILEINVALID) {
-               ast_mutex_unlock(&cdr_batch_lock);
+       if (channel_subscription || bridge_subscription || parking_subscription) {
                return 0;
        }
 
-       /* don't run the next scheduled CDR posting while reloading */
-       AST_SCHED_DEL(sched, cdr_sched);
+       channel_subscription = stasis_forward_all(ast_channel_topic_all_cached(), cdr_topic);
+       if (!channel_subscription) {
+               return -1;
+       }
+       bridge_subscription = stasis_forward_all(ast_bridge_topic_all_cached(), cdr_topic);
+       if (!bridge_subscription) {
+               return -1;
+       }
+       parking_subscription = stasis_forward_all(ast_parking_topic(), cdr_topic);
+       if (!parking_subscription) {
+               return -1;
+       }
 
-       if (config) {
-               if ((enabled_value = ast_variable_retrieve(config, "general", "enable"))) {
-                       enabled = ast_true(enabled_value);
-               }
-               if ((unanswered_value = ast_variable_retrieve(config, "general", "unanswered"))) {
-                       unanswered = ast_true(unanswered_value);
-               }
-               if ((batched_value = ast_variable_retrieve(config, "general", "batch"))) {
-                       batchmode = ast_true(batched_value);
-               }
-               if ((scheduleronly_value = ast_variable_retrieve(config, "general", "scheduleronly"))) {
-                       batchscheduleronly = ast_true(scheduleronly_value);
-               }
-               if ((batchsafeshutdown_value = ast_variable_retrieve(config, "general", "safeshutdown"))) {
-                       batchsafeshutdown = ast_true(batchsafeshutdown_value);
-               }
-               if ((size_value = ast_variable_retrieve(config, "general", "size"))) {
-                       if (sscanf(size_value, "%30d", &cfg_size) < 1)
-                               ast_log(LOG_WARNING, "Unable to convert '%s' to a numeric value.\n", size_value);
-                       else if (cfg_size < 0)
-                               ast_log(LOG_WARNING, "Invalid maximum batch size '%d' specified, using default\n", cfg_size);
-                       else
-                               batchsize = cfg_size;
-               }
-               if ((time_value = ast_variable_retrieve(config, "general", "time"))) {
-                       if (sscanf(time_value, "%30d", &cfg_time) < 1)
-                               ast_log(LOG_WARNING, "Unable to convert '%s' to a numeric value.\n", time_value);
-                       else if (cfg_time < 0)
-                               ast_log(LOG_WARNING, "Invalid maximum batch time '%d' specified, using default\n", cfg_time);
-                       else
-                               batchtime = cfg_time;
-               }
-               if ((end_before_h_value = ast_variable_retrieve(config, "general", "endbeforehexten")))
-                       ast_set2_flag(&ast_options, ast_true(end_before_h_value), AST_OPT_FLAG_END_CDR_BEFORE_H_EXTEN);
-               if ((initiatedseconds_value = ast_variable_retrieve(config, "general", "initiatedseconds")))
-                       ast_set2_flag(&ast_options, ast_true(initiatedseconds_value), AST_OPT_FLAG_INITIATED_SECONDS);
-       }
-
-       if (enabled && !batchmode) {
-               ast_log(LOG_NOTICE, "CDR simple logging enabled.\n");
-       } else if (enabled && batchmode) {
-               cdr_sched = ast_sched_add(sched, batchtime * 1000, submit_scheduled_batch, NULL);
-               ast_log(LOG_NOTICE, "CDR batch mode logging enabled, first of either size %d or time %d seconds.\n", batchsize, batchtime);
-       } else {
-               ast_log(LOG_NOTICE, "CDR logging disabled, data will be lost.\n");
+       return 0;
+}
+
+static int process_config(int reload)
+{
+       RAII_VAR(struct module_config *, mod_cfg, module_config_alloc(), ao2_cleanup);
+
+       if (!reload) {
+               if (aco_info_init(&cfg_info)) {
+                       return 1;
+               }
+
+               aco_option_register(&cfg_info, "enable", ACO_EXACT, general_options, DEFAULT_ENABLED, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_ENABLED);
+               aco_option_register(&cfg_info, "debug", ACO_EXACT, general_options, 0, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_DEBUG);
+               aco_option_register(&cfg_info, "unanswered", ACO_EXACT, general_options, DEFAULT_UNANSWERED, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_UNANSWERED);
+               aco_option_register(&cfg_info, "congestion", ACO_EXACT, general_options, 0, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_CONGESTION);
+               aco_option_register(&cfg_info, "batch", ACO_EXACT, general_options, DEFAULT_BATCHMODE, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_BATCHMODE);
+               aco_option_register(&cfg_info, "endbeforehexten", ACO_EXACT, general_options, DEFAULT_END_BEFORE_H_EXTEN, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_END_BEFORE_H_EXTEN);
+               aco_option_register(&cfg_info, "initiatedseconds", ACO_EXACT, general_options, DEFAULT_INITIATED_SECONDS, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_INITIATED_SECONDS);
+               aco_option_register(&cfg_info, "scheduleronly", ACO_EXACT, general_options, DEFAULT_BATCH_SCHEDULER_ONLY, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, batch_settings.settings), BATCH_MODE_SCHEDULER_ONLY);
+               aco_option_register(&cfg_info, "safeshutdown", ACO_EXACT, general_options, DEFAULT_BATCH_SAFE_SHUTDOWN, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, batch_settings.settings), BATCH_MODE_SAFE_SHUTDOWN);
+               aco_option_register(&cfg_info, "size", ACO_EXACT, general_options, DEFAULT_BATCH_SIZE, OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct ast_cdr_config, batch_settings.size), 0, MAX_BATCH_SIZE);
+               aco_option_register(&cfg_info, "time", ACO_EXACT, general_options, DEFAULT_BATCH_TIME, OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct ast_cdr_config, batch_settings.time), 0, MAX_BATCH_TIME);
+       }
+
+       if (aco_process_config(&cfg_info, reload)) {
+               if (!mod_cfg) {
+                       return 1;
+               }
+               /* If we couldn't process the configuration and this wasn't a reload,
+                * create a default config
+                */
+               if (!reload && !(aco_set_defaults(&general_option, "general", mod_cfg->general))) {
+                       ast_log(LOG_NOTICE, "Failed to process CDR configuration; using defaults\n");
+                       ao2_global_obj_replace_unref(module_configs, mod_cfg);
+                       return 0;
+               }
+               return 1;
        }
 
-       /* if this reload enabled the CDR batch mode, create the background thread
-          if it does not exist */
-       if (enabled && batchmode && (!was_enabled || !was_batchmode) && (cdr_thread == AST_PTHREADT_NULL)) {
+       return 0;
+}
+
+static void cdr_engine_cleanup(void)
+{
+       destroy_subscriptions();
+}
+
+static void cdr_engine_shutdown(void)
+{
+       stasis_message_router_unsubscribe_and_join(stasis_router);
+       stasis_router = NULL;
+
+       ao2_cleanup(cdr_topic);
+       cdr_topic = NULL;
+
+       STASIS_MESSAGE_TYPE_CLEANUP(cdr_sync_message_type);
+
+       ao2_callback(active_cdrs_by_channel, OBJ_NODATA, cdr_object_dispatch_all_cb,
+               NULL);
+       finalize_batch_mode();
+       ast_cli_unregister_multiple(cli_commands, ARRAY_LEN(cli_commands));
+       ast_sched_context_destroy(sched);
+       sched = NULL;
+       ast_free(batch);
+       batch = NULL;
+
+       aco_info_destroy(&cfg_info);
+       ao2_global_obj_release(module_configs);
+
+       ao2_container_unregister("cdrs_by_channel");
+       ao2_ref(active_cdrs_by_channel, -1);
+       active_cdrs_by_channel = NULL;
+}
+
+static void cdr_enable_batch_mode(struct ast_cdr_config *config)
+{
+       SCOPED_LOCK(batch, &cdr_batch_lock, ast_mutex_lock, ast_mutex_unlock);
+
+       /* Only create the thread level portions once */
+       if (cdr_thread == AST_PTHREADT_NULL) {
                ast_cond_init(&cdr_pending_cond, NULL);
                if (ast_pthread_create_background(&cdr_thread, NULL, do_cdr, NULL) < 0) {
                        ast_log(LOG_ERROR, "Unable to start CDR thread.\n");
-                       AST_SCHED_DEL(sched, cdr_sched);
+                       return;
+               }
+       }
+
+       /* Kill the currently scheduled item */
+       AST_SCHED_DEL(sched, cdr_sched);
+       cdr_sched = ast_sched_add(sched, config->batch_settings.time * 1000, submit_scheduled_batch, NULL);
+       ast_log(LOG_NOTICE, "CDR batch mode logging enabled, first of either size %u or time %u seconds.\n",
+                       config->batch_settings.size, config->batch_settings.time);
+}
+
+/*!
+ * \internal
+ * \brief Print channel object key (name).
+ * \since 12.0.0
+ *
+ * \param v_obj A pointer to the object we want the key printed.
+ * \param where User data needed by prnt to determine where to put output.
+ * \param prnt Print output callback function to use.
+ *
+ * \return Nothing
+ */
+static void cdr_container_print_fn(void *v_obj, void *where, ao2_prnt_fn *prnt)
+{
+       struct cdr_object *cdr = v_obj;
+       struct cdr_object *it_cdr;
+       if (!cdr) {
+               return;
+       }
+       for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+               prnt(where, "Party A: %s; Party B: %s; Bridge %s\n", it_cdr->party_a.snapshot->name, it_cdr->party_b.snapshot ? it_cdr->party_b.snapshot->name : "<unknown>",
+                               it_cdr->bridge);
+       }
+}
+
+/*!
+ * \brief Checks if CDRs are enabled and enables/disables the necessary options
+ */
+static int cdr_toggle_runtime_options(void)
+{
+       RAII_VAR(struct module_config *, mod_cfg,
+               ao2_global_obj_ref(module_configs), ao2_cleanup);
+
+       if (ast_test_flag(&mod_cfg->general->settings, CDR_ENABLED)) {
+               if (create_subscriptions()) {
+                       destroy_subscriptions();
+                       ast_log(AST_LOG_ERROR, "Failed to create Stasis subscriptions\n");
+                       return -1;
+               }
+               if (ast_test_flag(&mod_cfg->general->settings, CDR_BATCHMODE)) {
+                       cdr_enable_batch_mode(mod_cfg->general);
                } else {
-                       ast_cli_register(&cli_submit);
-                       ast_register_atexit(ast_cdr_engine_term);
-                       res = 0;
-               }
-       /* if this reload disabled the CDR and/or batch mode and there is a background thread,
-          kill it */
-       } else if (((!enabled && was_enabled) || (!batchmode && was_batchmode)) && (cdr_thread != AST_PTHREADT_NULL)) {
-               /* wake up the thread so it will exit */
-               pthread_cancel(cdr_thread);
-               pthread_kill(cdr_thread, SIGURG);
-               pthread_join(cdr_thread, NULL);
-               cdr_thread = AST_PTHREADT_NULL;
-               ast_cond_destroy(&cdr_pending_cond);
-               ast_cli_unregister(&cli_submit);
-               ast_unregister_atexit(ast_cdr_engine_term);
-               res = 0;
-               /* if leaving batch mode, then post the CDRs in the batch,
-                  and don't reschedule, since we are stopping CDR logging */
-               if (!batchmode && was_batchmode) {
-                       ast_cdr_engine_term();
+                       ast_log(LOG_NOTICE, "CDR simple logging enabled.\n");
                }
        } else {
-               res = 0;
+               destroy_subscriptions();
+               ast_log(LOG_NOTICE, "CDR logging disabled.\n");
        }
 
-       ast_mutex_unlock(&cdr_batch_lock);
-       ast_config_destroy(config);
-       manager_event(EVENT_FLAG_SYSTEM, "Reload", "Module: CDR\r\nMessage: CDR subsystem reload requested\r\n");
-
-       return res;
+       return 0;
 }
 
 int ast_cdr_engine_init(void)
 {
-       int res;
+       if (process_config(0)) {
+               return -1;
+       }
+
+       cdr_topic = stasis_topic_create("cdr_engine");
+       if (!cdr_topic) {
+               return -1;
+       }
+
+       stasis_router = stasis_message_router_create(cdr_topic);
+       if (!stasis_router) {
+               return -1;
+       }
+
+       if (STASIS_MESSAGE_TYPE_INIT(cdr_sync_message_type)) {
+               return -1;
+       }
+
+       stasis_message_router_add_cache_update(stasis_router, ast_channel_snapshot_type(), handle_channel_cache_message, NULL);
+       stasis_message_router_add(stasis_router, ast_channel_dial_type(), handle_dial_message, NULL);
+       stasis_message_router_add(stasis_router, ast_channel_entered_bridge_type(), handle_bridge_enter_message, NULL);
+       stasis_message_router_add(stasis_router, ast_channel_left_bridge_type(), handle_bridge_leave_message, NULL);
+       stasis_message_router_add(stasis_router, ast_parked_call_type(), handle_parked_call_message, NULL);
+       stasis_message_router_add(stasis_router, cdr_sync_message_type(), handle_cdr_sync_message, NULL);
+
+       active_cdrs_by_channel = ao2_container_alloc(NUM_CDR_BUCKETS,
+               cdr_object_channel_hash_fn, cdr_object_channel_cmp_fn);
+       if (!active_cdrs_by_channel) {
+               return -1;
+       }
+       ao2_container_register("cdrs_by_channel", active_cdrs_by_channel, cdr_container_print_fn);
 
        sched = ast_sched_context_create();
        if (!sched) {
@@ -1622,74 +4203,66 @@ int ast_cdr_engine_init(void)
                return -1;
        }
 
-       ast_cli_register(&cli_status);
-
-       res = do_reload(0);
-       if (res) {
-               ast_mutex_lock(&cdr_batch_lock);
-               res = init_batch();
-               ast_mutex_unlock(&cdr_batch_lock);
-       }
+       ast_cli_register_multiple(cli_commands, ARRAY_LEN(cli_commands));
+       ast_register_cleanup(cdr_engine_cleanup);
+       ast_register_atexit(cdr_engine_shutdown);
 
-       return res;
+       return cdr_toggle_runtime_options();
 }
 
-/* \note This actually gets called a couple of times at shutdown.  Once, before we start
-   hanging up channels, and then again, after the channel hangup timeout expires */
 void ast_cdr_engine_term(void)
 {
-       ast_cdr_submit_batch(batchsafeshutdown);
-}
+       RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       RAII_VAR(void *, payload, NULL, ao2_cleanup);
+       RAII_VAR(struct stasis_message *, message, NULL, ao2_cleanup);
+
+       /* Since this is called explicitly during process shutdown, we might not have ever
+        * been initialized. If so, the config object will be NULL.
+        */
+       if (!mod_cfg) {
+               return;
+       }
 
-int ast_cdr_engine_reload(void)
-{
-       return do_reload(1);
+       if (cdr_sync_message_type()) {
+               /* Make sure we have the needed items */
+               payload = ao2_alloc(sizeof(*payload), NULL);
+               if (!stasis_router || !payload) {
+                       return;
+               }
+
+               ast_debug(1, "CDR Engine termination request received; waiting on messages...\n");
+
+               message = stasis_message_create(cdr_sync_message_type(), payload);
+               if (message) {
+                       stasis_message_router_publish_sync(stasis_router, message);
+               }
+       }
+
+       if (ast_test_flag(&mod_cfg->general->settings, CDR_BATCHMODE)) {
+               cdr_submit_batch(ast_test_flag(&mod_cfg->general->batch_settings.settings, BATCH_MODE_SAFE_SHUTDOWN));
+       }
 }
 
-int ast_cdr_data_add_structure(struct ast_data *tree, struct ast_cdr *cdr, int recur)
+int ast_cdr_engine_reload(void)
 {
-       struct ast_cdr *tmpcdr;
-       struct ast_data *level;
-       struct ast_var_t *variables;
-       const char *var, *val;
-       int x = 1, i;
-       char workspace[256];
-       char *tmp;
+       RAII_VAR(struct module_config *, old_mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
+       RAII_VAR(struct module_config *, mod_cfg, NULL, ao2_cleanup);
 
-       if (!cdr) {
+       if (process_config(1)) {
                return -1;
        }
 
-       for (tmpcdr = cdr; tmpcdr; tmpcdr = (recur ? tmpcdr->next : NULL)) {
-               level = ast_data_add_node(tree, "level");
-               if (!level) {
-                       continue;
-               }
-
-               ast_data_add_int(level, "level_number", x);
-
-               AST_LIST_TRAVERSE(&tmpcdr->varshead, variables, entries) {
-                       if (variables && (var = ast_var_name(variables)) &&
-                                       (val = ast_var_value(variables)) && !ast_strlen_zero(var)
-                                       && !ast_strlen_zero(val)) {
-                               ast_data_add_str(level, var, val);
-                       } else {
-                               break;
-                       }
-               }
+       mod_cfg = ao2_global_obj_ref(module_configs);
 
-               for (i = 0; cdr_readonly_vars[i]; i++) {
-                       workspace[0] = 0; /* null out the workspace, because the cdr_get_tv() won't write anything if time is NULL, so you get old vals */
-                       ast_cdr_getvar(tmpcdr, cdr_readonly_vars[i], &tmp, workspace, sizeof(workspace), 0, 0);
-                       if (!tmp) {
-                               continue;
-                       }
-                       ast_data_add_str(level, cdr_readonly_vars[i], tmp);
+       if (!ast_test_flag(&mod_cfg->general->settings, CDR_ENABLED) ||
+                       !(ast_test_flag(&mod_cfg->general->settings, CDR_BATCHMODE))) {
+               /* If batch mode used to be enabled, finalize the batch */
+               if (ast_test_flag(&old_mod_cfg->general->settings, CDR_BATCHMODE)) {
+                       finalize_batch_mode();
                }
-
-               x++;
        }
 
-       return 0;
+       return cdr_toggle_runtime_options();
 }
 
+