Announcements
We are currently migrating data within this board to improve the community. During this process, posting, replying, editing and other features are temporarily disabled. We sincerely apologize for any inconvenience caused and appreciate your understanding. Thank you for your patience and support.

Garbage Collection throws a System.InvalidOperationException ?

Garbage Collection throws a System.InvalidOperationException ?

Joshua_claytonWV5WP
Enthusiast Enthusiast
2,003 Views
25 Replies
Message 1 of 26

Garbage Collection throws a System.InvalidOperationException ?

Joshua_claytonWV5WP
Enthusiast
Enthusiast

Joshua_claytonWV5WP_0-1704216261046.png

I have a command that will iterate over a Bill Of Materials table that opens individual drawings and performs some actions on them one-by-one.

It appears that the Garbage Collector is throwing an error (System.InvalidOperationException) that I can't catch and causes AutoCAD to crash. "Operation is invalid due to the current state of the object."

Initially, I had the Transaction in a using statement, but it would throw this error after the function was finished and Garbage Collection ran on its own.

 

What might cause the Transaction to not be disposable either manually or by the Garbage Collector?

 

Reply
Reply
0 Likes
Accepted solutions (1)
2,004 Views
25 Replies
Replies (25)
Message 2 of 26

ActivistInvestor
Mentor
Mentor

First we need to know what execution context your code is running in. Is it the application context or the document context?

 

An exception can occur during a GC if an API object that is supposed to be deterministically-disposed, is not disposed, and the code that runs from its finalizer or underlying native destructor is not thread-safe, and the garbage collector is not running on the main thread, which I believe is not the case. I'm pretty sure that the GC runs in a high-priority background thread even when you explicitly call Collect().

Reply
Reply
0 Likes
Message 3 of 26

Joshua_claytonWV5WP
Enthusiast
Enthusiast

I believe you are correct about the Garbage Collector running in a background thread - hence my inability to catch the error its throwing. Is there anyway to catch this type of error in C# ?

 

I'm not sure I totally understand your first question...

The code opens one or more DWG files one by one inside AutoCAD via:

Document doc = Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.Open(bomRow.ConstructionDwgPath, false);

 I then end up passing the `doc` variable to the subsequent code to make sure it's running on the correct drawing (even though it should be the active one anyway).

 

The issue arises in calling the next function that operates on that document and opens a transaction via:

if (doc == null)
{
    doc = Autodesk.AutoCAD.ApplicationServices.Core.Application.DocumentManager.MdiActiveDocument;
}
else
{
    Autodesk.AutoCAD.ApplicationServices.Core.Application.DocumentManager.MdiActiveDocument = doc;
}

Database currDb = doc.Database;

Transaction trans = currDb.TransactionManager.StartTransaction();

I initially had it open the transaction in a using statement, but the same error would occur where it can't dispose correctly.

There are a few objects it grabs with that transaction, then I try and either manually dispose of it on an error or commit then dispose, but none of those work. It always is throwing an error saying "Operation is invalid due to the current state of the object."

 

Reply
Reply
0 Likes
Message 4 of 26

ActivistInvestor
Mentor
Mentor

Sorry but little Snippets of code aren't telling enough about what's going on.

 

If your code is running in the application context then you must lock the document before you can access it.

Reply
Reply
0 Likes
Message 5 of 26

Joshua_claytonWV5WP
Enthusiast
Enthusiast

Sorry, was hoping I didn't need to include a huge chunk of code here - but if you're able to discern anything from it that's great.

 

If you look at line 309 in the code below it's already wrapped in a DocumentLock() right before the StartTransaction().

 

Update: code snippet removed

Reply
Reply
0 Likes
Message 6 of 26

ActivistInvestor
Mentor
Mentor

Your code  calls Dispose() on the Document's Database:

 

Database currDb = doc.Database;

............

currDb.Dispose();

 

Aside from that, you should  consider refactoring your code into manageable units, rather than piling it into one method, as that makes it much harder to follow the code flow, and makes it harder for you and others to diagnose problems.

Reply
Reply
Message 7 of 26

Joshua_claytonWV5WP
Enthusiast
Enthusiast

Thanks for taking the time to look this over. I agree it's messy. I inherited this code from a former developer and haven't had to time to refactor it yet.

 

I will try it without the disposal of the `currDb` variable - though I believe the transaction is failing to Commit() and Dispose() but earlier in the code.

The initial error I get is on line 914 with the trans.Commit().

Subsequent calls to dispose of it fail in the same manner.

 

I should say that on line 887 that function call to `DeleteExtraPartLayouts()` calls trans.Commit() as well - is this the culprit?

public void DeleteExtraPartLayouts(Transaction trans, Database db, Document doc = null)
{
	if (doc == null)
	{
        doc = Autodesk.AutoCAD.ApplicationServices.Core.Application.DocumentManager.MdiActiveDocument;
    }
	
	using (DocumentLock docLock = doc.LockDocument())
	{

		DBDictionary layoutDict2 = (DBDictionary)trans.GetObject(db.LayoutDictionaryId, OpenMode.ForWrite);

		if (layoutDict2.Count > 2)
		{
			foreach (DBDictionaryEntry de in layoutDict2)
			{

				if (!(de.Key.ToUpper().Contains("MODEL") || de.Key.ToUpper().Contains("8.5X11 PART") | de.Key.ToUpper().Contains("PART DRW")))
				{
                   
					LayoutManager.Current.DeleteLayout(de.Key);
				}
			}
			trans.Commit();
		}


		doc.Editor.Regen();
	}
}

 

 

Reply
Reply
0 Likes
Message 8 of 26

ActivistInvestor
Mentor
Mentor

Multiple calls to Commit() could be a cause.

 

You can replace all calls to Commit() with calls to the following extension method, which would avoid that problem:

 

public static class DatabaseExtensions
{
   public static bool TryCommit(this Transaction tr)
   {
      if(tr == null)
         throw new ArgumentNullException(nameof(tr));
      bool result = tr.AutoDelete;
      if(result)
         tr.Commit();
      return result;
   }
}

 

Reply
Reply
Message 9 of 26

Joshua_claytonWV5WP
Enthusiast
Enthusiast

Trying the extension method out results in a new error ?

Joshua_claytonWV5WP_0-1704309517459.png

 

Reply
Reply
0 Likes
Message 10 of 26

kerry_w_brown
Advisor
Advisor

@Joshua_claytonWV5WP 

 

Just a comment , without grocking the entire post . . 

What is the reason for commenting out the using Transaction ?

//                  using (Transaction trans = currDb.TransactionManager.StartTransaction())
			//{

 Regards,


// Called Kerry or kdub in my other life.

Everything will work just as you expect it to, unless your expectations are incorrect. ~ kdub
Sometimes the question is more important than the answer. ~ kdub

NZST UTC+12

class keyThumper<T> : Lazy<T>;      another  Swamper

Reply
Reply
0 Likes
Message 11 of 26

Joshua_claytonWV5WP
Enthusiast
Enthusiast

@kerry_w_brown:

It was still causing a Garbage Collection error whenever it decides to run based on the transaction variable state.

Reply
Reply
0 Likes
Message 12 of 26

kerry_w_brown
Advisor
Advisor

So, you are trying to write your own transaction management functions to replace the API one you commented out ?

 

Regards,


// Called Kerry or kdub in my other life.

Everything will work just as you expect it to, unless your expectations are incorrect. ~ kdub
Sometimes the question is more important than the answer. ~ kdub

NZST UTC+12

class keyThumper<T> : Lazy<T>;      another  Swamper

Reply
Reply
0 Likes
Message 13 of 26

Joshua_claytonWV5WP
Enthusiast
Enthusiast

It's not ideal, but the hope was to catch the Garbage Collector's exception as it's not causing issues on the drawing operations.

Per the SDK - it's not invalid? (though, not recommended) to not use a using statement. If I do, it will simply crash uncontrollably (as well) later whenever Garbage Collection decides to run.

Something is causing the transaction to go into a weird state and I cannot determine what/where.

Reply
Reply
0 Likes
Message 14 of 26

kerry_w_brown
Advisor
Advisor

@Joshua_claytonWV5WP 

 

Personally, I'd do two things ;

 

Refactor the code into managable, conceptual chunks, as Tony recommened.

(also, there seems to be a lot of guard clause statement blocks that could be refactored into wellnamed method calls)

Step through the code using breakpoints in an attempt to isolate the portion of code that is causing grief.


Regards,



// Called Kerry or kdub in my other life.

Everything will work just as you expect it to, unless your expectations are incorrect. ~ kdub
Sometimes the question is more important than the answer. ~ kdub

NZST UTC+12

class keyThumper<T> : Lazy<T>;      another  Swamper

Reply
Reply
0 Likes
Message 15 of 26

norman.yuan
Mentor
Mentor

As other have already pointed out that the code is really hard to follow/read to figure out how you use the transaction, so I really do not have ideas on the code logic that may be the cause of the error of GC.Collect().

 

However, I am very curious that why there is the need to call GC.Collect() explicitly, rather than letting .NET runtime does it automatically behand the scene? To be honest, after all these years of coding .NET framework with AutoCAD API, I never did or thought to force garbage collection. Do your process load large number of disposable objects created by your code and you feel the responsibility to remove them from the memory afterward? From your code and your brief description, I do not see that. Rather, your code opens AutoCAD entities existing in drawing via a Transaction. In AutoCAD .NET API coding, the thumb of rule is you only dispose the Acad objects your code CREATED and not add them into a transaction. For Acad Oojects existing in the AutoCAD process, such as MdiActiveDocument's Database, you do not dispose it; and for existing DBObjects opened in a transaction, you do not dispose them, the Transaction does it for you. Also, since most disposable objects is just a .NET wrapper of underline C++ objects, their disposal would depend on how the .NET wrappers were implemented, I'd leave GC.Collect() out of my code (as I said, I never used it).

 

I could see your code is doing some kind of batch processing, which may lead you to think GC.Collect() would release used memory by opening many drawings in the batch processing. I'd batch processing too many drawings would have memory fragmentation issue rather than garbage collection for releasing memory from disposed objects slightly earlier than .NET runtime does it automatically.

 

So, if I were you, assume the code does the batch processing logically sound and work as expected (which I think is hardly the case, sorry), I would just remove the GC.Collect() from the code, and let .NET runtime do its job.

 

If you indeed do batch process against so many drawing, or many very large drawing, that may quickly result in memory fragments and eventually dries up the available memory of the computer, you may want to use core console for the batch processing - one core console instance for one drawing, or even use Autodesk's Platform Services (online core console).

 

Norman Yuan

Drive CAD With Code

EESignature

Reply
Reply
Message 16 of 26

Joshua_claytonWV5WP
Enthusiast
Enthusiast

The direct request is to operate over several drawings (maybe 20 - 50 at a time) to submit them to our ERP system and SQL server as quickly as possible.

Otherwise, a user would have to open each one and submit them via a form that would take weeks for one person to accomplish with how many grouped drawings we are talking about.

 

I can disable the GC.Collect() calls and return the using statement, but I guarantee the same error will occur when Garbage Collection runs via .NET automatically. This is my issue and why I was trying to force it and catch errors. The transaction is getting to a weird state that is unbeknownst to me.

 

Update: as soon as I allow the using statement back, it fails on the first drawing when it reaches the end of the block - as expected :(.

Reply
Reply
0 Likes
Message 17 of 26

norman.yuan
Mentor
Mentor

So, it clearly indicates that the code has wrong logic of doing the work as you want it to do, thus wondering why GC.collect() raises error is barking on the wrong tree. You need to look into the code that does the work really hard to figure it out. Again, just for making your debugging easier, you really should reorganize/break down your code into smaller meaningful chunk BEFORE WASTING time to step through it. To be as frank as I can, if I was handed such messy code for debugging, I'd first rewrite it to make it "debug-able" before actually debugging.

 

Norman Yuan

Drive CAD With Code

EESignature

Reply
Reply
0 Likes
Message 18 of 26

kerry_w_brown
Advisor
Advisor

What are the indicators and result of the failure ?

As it stands I believe there is no way for any of us to actually test the Methods.

We are relying on you posting your current source code ( we can only peruse it )  and an accurate description of the circumstances.

Things like examining the locals pane at a breakpoint may help, but this would become very tedious for us (asking) and guessing causes. 

If you inherited this code, then the code that has been added/modified would be a good place to start looking.

Regards,

 

 


// Called Kerry or kdub in my other life.

Everything will work just as you expect it to, unless your expectations are incorrect. ~ kdub
Sometimes the question is more important than the answer. ~ kdub

NZST UTC+12

class keyThumper<T> : Lazy<T>;      another  Swamper

Reply
Reply
0 Likes
Message 19 of 26

Joshua_claytonWV5WP
Enthusiast
Enthusiast

The overall project solution is roughly ~110,000 lines of code.

This part of it is about ~10,000 lines of that scattered through various files.

It makes an active connection to our ERP system and SQL server, so sharing it wouldn't be run-able without a lot of modifications.

 

The modifications that I made to the current code were to mostly bypass forms being presented to user via a single part publish form and make assumptions based on the information gathered for each part.

 

Would probably require a NDA to share much of anything else - though I am open to seeing if we could have a 3rd party help further.

 

Thanks for the brainstorming.

Reply
Reply
0 Likes
Message 20 of 26

ActivistInvestor
Mentor
Mentor

The extension method must be in a static class you don't show that you only show the extension method. The exception is suggesting that you passed a null argument to the method.

Reply
Reply
0 Likes