Another Recursive Hierarchy bit of help needed...

  • If theres one thing I particularly suck at with SQL its recursive hierarchies.. my brain shuts down....

    So I have this table that captures blocking details

    CREATE TABLE [dbo].[Blocking](

    [spid] [int] NULL,

    [waitresource] [varchar](512) NULL,

    [typ_of_block] [varchar](20) NULL,

    [capdt] [datetime] NULL,

    [blocked_spid] [int] NULL

    )

    GO

    A typical row would be

    spid waitresource typ_of_block capdt blocked_spid

    3842:9:78831 Blocking 2015-12-07 09:13:29.43002

    407KEY: 21:72067595954855944 (d167984bf49a)Blocked 2015-12-07 09:57:11.140612

    I want to be able to display this in hierarchical format so it displays each lead blocker then blocked records for that lead in hierarchical fashion down to the bottom of the tree ... Ive tried and failed and have vanished down the rabbit hole ....

    help !!!

  • Might not be the answer that you are looking for, but I do think that this might help you. I'm using the procedure sp_blocked_process_report_viewer that was written by Michael J Swart and can be found at http://michaeljswart.com/2011/04/a-new-way-to-examine-blocked-process-reports/. I strongly recommend using this procedure to analyze blocking report done by server side trace or profiler.

    Adi

    --------------------------------------------------------------
    To know how to ask questions and increase the chances of getting asnwers:
    http://www.sqlservercentral.com/articles/Best+Practices/61537/

    For better answers on performance questions, click on the following...
    http://www.sqlservercentral.com/articles/SQLServerCentral/66909/

  • Recursive CTEs aren't bad once you get your head wrapped around them... If you want to post some consumable test data, I'll attempt a solution.

  • Jason A. Long (12/17/2015)


    Recursive CTEs aren't bad once you get your head wrapped around them... If you want to post some consumable test data, I'll attempt a solution.

    See thee first link in my signature line below for how to properly create readily consumable test data as Jason suggested above. It'll really help us help you much more quickly and accurately.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Adi Cohn-120898 (12/8/2015)


    Might not be the answer that you are looking for, but I do think that this might help you. I'm using the procedure sp_blocked_process_report_viewer that was written by Michael J Swart and can be found at http://michaeljswart.com/2011/04/a-new-way-to-examine-blocked-process-reports/. I strongly recommend using this procedure to analyze blocking report done by server side trace or profiler.

    Adi

    The problem I have with that fine article is that you have to know the name of the trace as well as having to set up an SQL Profiler trace even if it is a server side trace. Additionally...

    The alternative that I used was to setup an alert for blocking (Object = SQLServer:General Statistics, Counter = Processes blocked) which not only sends me an immediate email when extended blocking occurs but also runs a job to run a proc to capture the information from the sys.dm_exec_requests view and sys.dm_exec_sql_text functions into a permanent table in my "DBA database" for further analysis. That also means that I don't have to mess with any bloody XML or remember that I have a file on the server growing in the background. I also don't have to remember to start SQL Profiler after a server bounce for upgrades or whatever.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Thanks Jeff for the handy hint on the sort of data required and how to post it .. heres a very small subset of data, one chain with a single lead blocker and one with a depth of three

    CREATE TABLE [dbo].[Blocking](

    [spid] [int] NULL,

    [typ_of_block] [varchar](20) NULL,

    [capdt] [datetime] NULL,

    [blocked_spid] [int] NULL

    )

    GO

    insert blocking

    values ('84','Blocking','2015-12-15 05:56:44.677','0')

    insert blocking

    values ('85','Blocked','2015-12-15 05:56:44.677','84')

    insert blocking

    values ('106','Blocked','2015-12-15 07:46:25.107','158')

    insert blocking

    values ('158','Blocking','2015-12-15 07:46:25.107','0')

    insert blocking

    values ('253','Blocked','2015-12-15 07:46:25.107','106')

    insert blocking

    values ('299','Blocked','2015-12-15 07:48:33.710','106')

  • simon_s (12/21/2015)


    Thanks Jeff for the handy hint on the sort of data required and how to post it .. heres a very small subset of data, one chain with a single lead blocker and one with a depth of three

    CREATE TABLE [dbo].[Blocking](

    [spid] [int] NULL,

    [typ_of_block] [varchar](20) NULL,

    [capdt] [datetime] NULL,

    [blocked_spid] [int] NULL

    )

    GO

    insert blocking

    values ('84','Blocking','2015-12-15 05:56:44.677','0')

    insert blocking

    values ('85','Blocked','2015-12-15 05:56:44.677','84')

    insert blocking

    values ('106','Blocked','2015-12-15 07:46:25.107','158')

    insert blocking

    values ('158','Blocking','2015-12-15 07:46:25.107','0')

    insert blocking

    values ('253','Blocked','2015-12-15 07:46:25.107','106')

    insert blocking

    values ('299','Blocked','2015-12-15 07:48:33.710','106')

    Bit confused by the data and the titles of the columns. In the last to inserts above, is looks like SPID 106 is blocking SPIDs 253 and 299 rather than the other way around as the column names you provided would suggest. I'm pretty sure that it's not possible for two SPIDs to block a given SPID.

    Heh... never mind. Not enough coffee. That's answers my own question.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Jeff will no doubt follow up with something super cool... but in the mean time, this should give you the basic idea...

    IF OBJECT_ID('tempdb..#Blocking', 'U') IS NOT NULL

    DROP TABLE #Blocking;

    CREATE TABLE #Blocking(

    [spid] [int] NULL,

    [typ_of_block] [varchar](20) NULL,

    [capdt] [datetime] NULL,

    [blocked_spid] [int] NULL

    );

    GO

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('84','Blocking','2015-12-15 05:56:44.677','0');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('85','Blocked','2015-12-15 05:56:44.677','84');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('106','Blocked','2015-12-15 07:46:25.107','158');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('158','Blocking','2015-12-15 07:46:25.107','0');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('253','Blocked','2015-12-15 07:46:25.107','106');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('299','Blocked','2015-12-15 07:48:33.710','106');

    WITH

    cte_Blocks AS (

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = CAST(1 AS INT),

    blocking_chain = CAST(b.spid AS VARCHAR(8000))

    FROM

    #Blocking b

    WHERE

    b.blocked_spid = 0

    UNION ALL

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = cb.blocked_level + 1,

    blocking_chain = CAST(CONCAT(cb.blocking_chain, '>', CAST(b.spid AS varchar(8000))) AS VARCHAR(8000))

    FROM

    #Blocking b

    JOIN cte_Blocks cb

    ON cb.spid = b.blocked_spid

    )

    SELECT

    cb.spid,

    cb.typ_of_block,

    cb.capdt,

    cb.blocked_spid,

    cb.blocked_level,

    cb.blocking_chain

    FROM

    cte_Blocks cb

    ORDER BY

    cb.capdt,

    cb.blocking_chain;

    Results:

    spid typ_of_block capdt blocked_spid blocked_level blocking_chain

    ----------- -------------------- ----------------------- ------------ ------------- -----------------------

    84 Blocking 2015-12-15 05:56:44.677 0 1 84

    85 Blocked 2015-12-15 05:56:44.677 84 2 84>85

    158 Blocking 2015-12-15 07:46:25.107 0 1 158

    106 Blocked 2015-12-15 07:46:25.107 158 2 158>106

    253 Blocked 2015-12-15 07:46:25.107 106 3 158>106>253

    299 Blocked 2015-12-15 07:48:33.710 106 3 158>106>299

  • Jason A. Long (12/21/2015)


    Jeff will no doubt follow up with something super cool...

    Not so much as the hierarchical query goes. You pretty much topped that off quite nicely. The only thing that you might want to add is a check to see if a SPID has already been added to the hierarchical path so that when a deadlock happens (a loop of spids blocking each other), you don't get an error in the code. You could actually use that detector to add a marker to the path to indicate that a deadlock had occurred.

    If the OP also does an APPLY between the sys.dm_exec_requests that I believe he may be using to capture the information and the sys.dm_exec_sql_text([sql_handle]) function, you can also get a copy of the offending code for each line. If that's of interest, let me know and I'll also show you how to make the code "click-to-view" as it's actually written.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • Minor change to deal with deadlocks:

    WITH

    cte_Blocks AS (

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = CAST(1 AS INT),

    blocking_chain = CAST(b.spid AS VARCHAR(8000))

    FROM

    #Blocking b

    WHERE b.blocked_spid = 0

    UNION ALL

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = cb.blocked_level + 1,

    blocking_chain = CAST(cb.blocking_chain + '>' + CAST(b.spid AS varchar(8000)) AS VARCHAR(8000))

    FROM

    #Blocking b

    JOIN cte_Blocks cb

    ON cb.spid = b.blocked_spid

    ),

    cte_Deadlocks AS (

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = CAST(1 AS INT),

    blocking_chain = CAST(b.spid AS VARCHAR(8000))

    FROM

    #Blocking b

    WHERE b.spid NOT IN (SELECT spid FROM cte_Blocks )

    UNION ALL

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = cb.blocked_level + 1,

    blocking_chain = CAST(cb.blocking_chain + '>' + CAST(b.spid AS varchar(8000)) AS VARCHAR(8000))

    + CASE WHEN cb.blocking_chain LIKE CAST(b.spid AS varchar(8000)) + '>%' THEN '!! DEADLOCK !!' ELSE '' END

    FROM

    #Blocking b

    JOIN cte_Deadlocks cb

    ON cb.spid = b.blocked_spid AND cb.blocking_chain NOT LIKE '%DEADLOCK%'

    )

    SELECT

    cb.spid,

    cb.typ_of_block,

    cb.capdt,

    cb.blocked_spid,

    cb.blocked_level,

    cb.blocking_chain

    FROM

    cte_Blocks cb

    UNION ALL

    SELECT

    cb.spid,

    cb.typ_of_block,

    cb.capdt,

    cb.blocked_spid,

    cb.blocked_level,

    cb.blocking_chain

    FROM

    cte_Deadlocks cb

    ORDER BY

    capdt,

    blocking_chain;

    And yes, CONCAT for some reason is not recognised by my SQL2008 server.

    Any idea why?

    :hehe:

    _____________
    Code for TallyGenerator

  • Jeff Moden


    Not so much as the hierarchical query goes. You pretty much topped that off quite nicely. The only thing that you might want to add is a check to see if a SPIC has already been added to the hierarchical path so that when a deadlock happens (a loop of spids blocking each other), you don't get an error in the code. You could actually use that detector to add a marker to the path to indicate that a deadlock had occurred.

    I like the idea... I was able to successfully break a loop by rejecting the "chain duplicate" row... But that's also rejecting the "indicator" row. (It's provably just late and I'm not wrapping my head all the way around it... At least I'm going to use it as my excuse...)

    In any case here's an updated test harness and my 1st stab at a solution... Aside from breaking up the infinite loop, I'm not in love with what I have so far...

    IF OBJECT_ID('tempdb..#Blocking', 'U') IS NOT NULL

    DROP TABLE #Blocking;

    CREATE TABLE #Blocking(

    [spid] [int] NULL,

    [typ_of_block] [varchar](20) NULL,

    [capdt] [datetime] NULL,

    [blocked_spid] [int] NULL

    );

    GO

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('84','Blocking','2015-12-15 05:56:44.677','0');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('85','Blocked','2015-12-15 05:56:44.677','84');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('106','Blocked','2015-12-15 07:46:25.107','158');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('158','Blocking','2015-12-15 07:46:25.107','0');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('253','Blocked','2015-12-15 07:46:25.107','106');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('299','Blocked','2015-12-15 07:48:33.710','106');

    -- new "looping" data rows ----------------------------

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('300','Blocked','2015-12-15 07:48:34.000','299');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('301','Blocked','2015-12-15 07:48:35.000','300');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('302','Blocked','2015-12-15 07:48:36.000','301');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('299','Blocked','2015-12-15 07:48:37.000','302');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('300','Blocked','2015-12-15 07:48:38.000','299');

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values ('301','Blocked','2015-12-15 07:48:39.000','300');

    WITH

    cte_Blocks AS (

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = CAST(1 AS INT),

    blocking_chain = CAST(b.spid AS VARCHAR(8000))

    FROM

    #Blocking b

    WHERE

    b.blocked_spid = 0

    UNION ALL

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = cb.blocked_level + 1,

    blocking_chain = CAST(CONCAT(cb.blocking_chain, '>', CAST(b.spid AS varchar(8000))) AS VARCHAR(8000))

    FROM

    #Blocking b

    JOIN cte_Blocks cb

    ON cb.spid = b.blocked_spid

    WHERE

    NOT EXISTS (SELECT 1 FROM dbo.DelimitedSplit8K(cb.blocking_chain, '>') sc WHERE b.spid = CAST(sc.Item AS INT))

    )

    SELECT

    cb.spid,

    cb.typ_of_block,

    capdt = MIN(cb.capdt),

    cb.blocked_spid,

    cb.blocked_level,

    cb.blocking_chain,

    dl_loops = COUNT(*)

    FROM

    cte_Blocks cb

    GROUP BY

    cb.spid,

    cb.typ_of_block,

    cb.blocked_spid,

    cb.blocked_level,

    cb.blocking_chain

    ORDER BY

    capdt,

    cb.blocking_chain

    ;

    Needless to say I'm interested to see your solution...

    Jeff Moden


    If the OP also does an APPLY between the sys.dm_exec_requests that I believe he may be using to capture the information and the sys.dm_exec_sql_text([sql_handle]) function, you can also get a copy of the offending code for each line. If that's of interest, let me know and I'll also show you how to make the code "click-to-view" as it's actually written.

    I can't speak for the OP, but I'm interested to see that as well...

  • Sergiy (12/21/2015)


    Minor change to deal with deadlocks:

    WITH

    cte_Blocks AS (

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = CAST(1 AS INT),

    blocking_chain = CAST(b.spid AS VARCHAR(8000))

    FROM

    #Blocking b

    WHERE b.blocked_spid = 0

    UNION ALL

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = cb.blocked_level + 1,

    blocking_chain = CAST(cb.blocking_chain + '>' + CAST(b.spid AS varchar(8000)) AS VARCHAR(8000))

    FROM

    #Blocking b

    JOIN cte_Blocks cb

    ON cb.spid = b.blocked_spid

    ),

    cte_Deadlocks AS (

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = CAST(1 AS INT),

    blocking_chain = CAST(b.spid AS VARCHAR(8000))

    FROM

    #Blocking b

    WHERE b.spid NOT IN (SELECT spid FROM cte_Blocks )

    UNION ALL

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = cb.blocked_level + 1,

    blocking_chain = CAST(cb.blocking_chain + '>' + CAST(b.spid AS varchar(8000)) AS VARCHAR(8000))

    + CASE WHEN cb.blocking_chain LIKE CAST(b.spid AS varchar(8000)) + '>%' THEN '!! DEADLOCK !!' ELSE '' END

    FROM

    #Blocking b

    JOIN cte_Deadlocks cb

    ON cb.spid = b.blocked_spid AND cb.blocking_chain NOT LIKE '%DEADLOCK%'

    )

    SELECT

    cb.spid,

    cb.typ_of_block,

    cb.capdt,

    cb.blocked_spid,

    cb.blocked_level,

    cb.blocking_chain

    FROM

    cte_Blocks cb

    UNION ALL

    SELECT

    cb.spid,

    cb.typ_of_block,

    cb.capdt,

    cb.blocked_spid,

    cb.blocked_level,

    cb.blocking_chain

    FROM

    cte_Deadlocks cb

    ORDER BY

    capdt,

    blocking_chain;

    Just ran this against my updated test harness... It didn't break the loop and got the "maximum recursion" error.

    Sergiy (12/21/2015)


    And yes, CONCAT for some reason is not recognised by my SQL2008 server.

    Any idea why?

    :hehe:

    I know exactly why... I'm a big dumb animal who wasn't paying attention to which sub-forum I was in... :doze: Mia Cupla...

  • -- removing duplicate post --

  • Jason A. Long (12/21/2015)


    Just ran this against my updated test harness... It didn't break the loop and got the "maximum recursion" error.

    I must admin - I have quite limited experience with deadlocking.

    I always try to prevent deadlocks from happening with an appropriate design and coding.

    So, when I face a deadlock situation there are no non-blocked transactions (the ones with Blocked=0) anymore.

    Therefore I did not consider an option of a deadlock within a tree growing from a non-blocked transaction.

    Well, it's easy to put the deadlock catching code into cte_Blocks too:

    WITH

    cte_Blocks AS (

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = CAST(1 AS INT),

    blocking_chain = CAST(b.spid AS VARCHAR(8000))

    FROM

    #Blocking b

    WHERE b.blocked_spid = 0

    UNION ALL

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = cb.blocked_level + 1,

    blocking_chain = CAST(cb.blocking_chain + '>' + CAST(b.spid AS varchar(8000)) AS VARCHAR(8000))

    + CASE WHEN cb.blocking_chain LIKE CAST(b.spid AS varchar(8000)) + '>%' or cb.blocking_chain LIKE '%>' + CAST(b.spid AS varchar(8000)) + '>%' THEN '!! DEADLOCK !!' ELSE '' END

    FROM

    #Blocking b

    JOIN cte_Blocks cb

    ON cb.spid = b.blocked_spid AND cb.blocking_chain NOT LIKE '%DEADLOCK%'

    ),

    cte_Deadlocks AS (

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = CAST(1 AS INT),

    blocking_chain = CAST(b.spid AS VARCHAR(8000))

    FROM

    #Blocking b

    WHERE b.spid NOT IN (SELECT spid FROM cte_Blocks )

    UNION ALL

    SELECT

    b.spid,

    b.typ_of_block,

    b.capdt,

    b.blocked_spid,

    blocked_level = cb.blocked_level + 1,

    blocking_chain = CAST(cb.blocking_chain + '>' + CAST(b.spid AS varchar(8000)) AS VARCHAR(8000))

    + CASE WHEN cb.blocking_chain LIKE CAST(b.spid AS varchar(8000)) + '>%' or cb.blocking_chain LIKE '%>' + CAST(b.spid AS varchar(8000)) + '>%' THEN '!! DEADLOCK !!' ELSE '' END

    FROM

    #Blocking b

    JOIN cte_Deadlocks cb

    ON cb.spid = b.blocked_spid AND cb.blocking_chain NOT LIKE '%DEADLOCK%'

    )

    SELECT

    cb.spid,

    cb.typ_of_block,

    cb.capdt,

    cb.blocked_spid,

    cb.blocked_level,

    cb.blocking_chain

    FROM

    cte_Blocks cb

    UNION ALL

    SELECT

    cb.spid,

    cb.typ_of_block,

    cb.capdt,

    cb.blocked_spid,

    cb.blocked_level,

    cb.blocking_chain

    FROM

    cte_Deadlocks cb

    ORDER BY

    capdt,

    blocking_chain;

    On another hand, you code, Jason, fails to catch the simplest classic case of a deadlock: when 2 or more transactions lock each other:

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values (1084,'Blocking','2015-12-15 05:56:44.677',1085);

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values (1085,'Blocked','2015-12-15 05:56:44.677',1186);

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values (1186,'Blocked','2015-12-15 05:56:44.677',1084);

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values (1010,'Blocking','2015-12-15 05:56:44.677',1011);

    insert #Blocking (spid,typ_of_block,capdt,blocked_spid)

    values (1011,'Blocked','2015-12-15 05:56:44.677',1010);

    Your code omits them completely.

    Which is not quite good, I think. 😎

    P.S. And you still used CONCAT - must be too hard to refuse. 🙂

    _____________
    Code for TallyGenerator

  • Yea... I'm right there with you. My latest test harness was based solely on my perception of what Jeff was talking about. I'm not certain that it's actually representative of a real, possible situation.

    Hopefully, Jeff will chime back in.

Viewing 15 posts - 1 through 15 (of 22 total)

You must be logged in to reply to this topic. Login to reply