#2651 - Add thread-safe FSUIPC integration#2655
#2651 - Add thread-safe FSUIPC integration#2655ghingres wants to merge 1 commit intoMobiFlight:mainfrom
Conversation
Fix bug MobiFlight#2651 FSUIPC Integration is not thread safe by changing the following a) Use thread safe ConcurrentDictionary in place of Dictionary b) Use a Lock to avoid FSUIPCConnection.Process() and setValue() are running in parallel threads c) Set ActionAtNextProcess to be OffsetAction.Write on any setValue() flow to avoid any Autosense issues (as advised by Peter of FSUIPC).
|
Thanks for the PR. I will have to take some time to properly think about this. |
|
I will let Copilot start with a review and see what it thinks, but I would really try to avoid copy/past repetition. |
| ConcurrentDictionary<Int32, Offset<Byte>> __cacheByte = new ConcurrentDictionary<Int32, Offset<Byte>>(); | ||
| ConcurrentDictionary<Int32, Offset<Int16>> __cacheShort = new ConcurrentDictionary<Int32, Offset<Int16>>(); | ||
| //ConcurrentDictionary<Int32, Offset<UInt16>> __cacheUShort = new ConcurrentDictionary<Int32, Offset<UInt16>>(); | ||
| ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>(); | ||
| //ConcurrentDictionary<Int32, Offset<UInt32>> __cacheUInt = new ConcurrentDictionary<Int32, Offset<UInt32>>(); | ||
| ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>(); | ||
| ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>(); | ||
| //ConcurrentDictionary<Int32, Offset<UInt64>> __cacheULong = new ConcurrentDictionary<Int32, Offset<UInt64>>(); | ||
| ConcurrentDictionary<Int32, Offset<Double>> __cacheDouble = new ConcurrentDictionary<Int32, Offset<Double>>(); | ||
| ConcurrentDictionary<Int32, Offset<String>> __cacheString = new ConcurrentDictionary<Int32, Offset<String>>(); |
There was a problem hiding this comment.
I think this is a good idea.
| lock (fsuipc_lock) | ||
| { | ||
| FSUIPCConnection.Process(); | ||
| } |
There was a problem hiding this comment.
I would introduce a method ProcessThreadSafe that wraps this call to avoid repetition of
lock (fsuipc_lock)
{
FSUIPCConnection.Process();
}
and just call "ProcessThreadSafe()"
this applies to all subsequent calls.
| lock (fsuipc_lock) | ||
| { | ||
| __cacheByte[offset] = new Offset<Byte>(offset); | ||
| _offsetsRegistered = true; | ||
| } | ||
| if (!__cacheByte.ContainsKey(offset)) | ||
| { | ||
| __cacheByte[offset] = new Offset<Byte>(offset); | ||
| _offsetsRegistered = true; | ||
| } | ||
|
|
||
| __cacheByte[offset].Value = value; | ||
| ((Offset<Byte>)__cacheByte[offset]).ActionAtNextProcess = OffsetAction.Write; | ||
| __cacheByte[offset].Value = value; | ||
|
|
||
| //long retval = this.getValue(offset, 1); | ||
| //if (retval != value) | ||
| //{ | ||
| // Log.Instance.log($"Recovered SetOffset() failure: Byte offset {offset}, value was {retval} after requesting {value}", LogSeverity.Warn); | ||
| // __cacheByte[offset].Value = value; | ||
| //} | ||
| } |
There was a problem hiding this comment.
With ConcurrentDictionary this is not necessary. ConcurrentDictionary is already thread-safe.
We have to check which methods are the correct ones to add additional keys to the Dictionary
There was a problem hiding this comment.
@DocMoebiuz - The original problem (as demonstrated by the commented out code) was occuring when I used ConcurrentDictionary. Even though threadsafe, the method believed it's setvalue was loaded when in fact a parallel FSUIPCConnection.Process() had overidden it's value ( The commented out code would fire and recover the situation). This was the testcase for proving where the locks were required. The setValue method and the FSUIPCConnection.Process() must not be operating in parallel. Peter confirmed the FSUIPCConnection.Process() is a synchonous process.
|
I totally missed the Set ActionAtNextProcess to be OffsetAction.Write on any setValue() to avoid any Autosense issues (as advised by Peter of FSUIPC). ... that's interesing, I was not aware of that. Does that mean that it ONLY performs writes that are pending for the next process? |
There was a problem hiding this comment.
Pull request overview
This pull request aims to fix bug #2651 by adding thread-safety to the FSUIPC integration layer. The changes address race conditions that could cause values to be lost when multiple threads simultaneously read and write FSUIPC offsets.
Changes:
- Converted Dictionary collections to ConcurrentDictionary for thread-safe storage of FSUIPC offset objects
- Added locks around all FSUIPCConnection.Process() and FSUIPCConnection.SendControlToFS() calls to prevent concurrent FSUIPC operations
- Set ActionAtNextProcess = OffsetAction.Write in all setOffset methods to ensure values are written on next Process call, as recommended by FSUIPC author
| //long retval = this.getValue(offset, 2); | ||
| //if (retval != value) | ||
| //{ | ||
| // Log.Instance.log($"Recovered SetOffset() failure: Short offset {offset}, value was {retval} after requesting {value}", LogSeverity.Warn); | ||
| // __cacheShort[offset].Value = value; | ||
| //} |
There was a problem hiding this comment.
Similar commented-out debugging code. Either remove it or add a comment explaining its purpose.
| if (!__cacheInt.ContainsKey(offset)) | ||
| { | ||
| __cacheInt[offset] = new Offset<Int32>(offset, writeOnly); | ||
| _offsetsRegistered = true; | ||
| } |
There was a problem hiding this comment.
Similar thread safety issue. Use TryAdd method instead: __cacheInt.TryAdd(offset, new Offset<Int32>(offset, writeOnly)).
| if (!__cacheDouble.ContainsKey(offset)) | ||
| { | ||
| __cacheDouble[offset] = new Offset<double>(offset); | ||
| _offsetsRegistered = true; | ||
| } |
There was a problem hiding this comment.
Similar thread safety issue. Use TryAdd method instead: __cacheDouble.TryAdd(offset, new Offset<double>(offset)).
| } | ||
|
|
||
| __cacheShort[offset].Value = value; | ||
| ((Offset<Int16>)__cacheShort[offset]).ActionAtNextProcess = OffsetAction.Write; |
There was a problem hiding this comment.
Similar to the Byte case, the cast to Offset is unnecessary. Consider removing it.
| ((Offset<Int16>)__cacheShort[offset]).ActionAtNextProcess = OffsetAction.Write; | |
| __cacheShort[offset].ActionAtNextProcess = OffsetAction.Write; |
| _offsetsRegistered = true; | ||
| } | ||
|
|
||
| ((Offset<double>)__cacheDouble[offset]).ActionAtNextProcess = OffsetAction.Write; |
There was a problem hiding this comment.
Similar to other cases, the cast to Offset is unnecessary. Consider removing it.
| ConcurrentDictionary<Int32, Offset<Byte>> __cacheByte = new ConcurrentDictionary<Int32, Offset<Byte>>(); | ||
| ConcurrentDictionary<Int32, Offset<Int16>> __cacheShort = new ConcurrentDictionary<Int32, Offset<Int16>>(); | ||
| //ConcurrentDictionary<Int32, Offset<UInt16>> __cacheUShort = new ConcurrentDictionary<Int32, Offset<UInt16>>(); | ||
| ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>(); |
There was a problem hiding this comment.
Field '__cacheInt' can be 'readonly'.
| ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>(); | |
| private readonly ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>(); |
| //ConcurrentDictionary<Int32, Offset<UInt16>> __cacheUShort = new ConcurrentDictionary<Int32, Offset<UInt16>>(); | ||
| ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>(); | ||
| //ConcurrentDictionary<Int32, Offset<UInt32>> __cacheUInt = new ConcurrentDictionary<Int32, Offset<UInt32>>(); | ||
| ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>(); |
There was a problem hiding this comment.
Field '__cacheFloat' can be 'readonly'.
| ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>(); | |
| readonly ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>(); |
| ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>(); | ||
| //ConcurrentDictionary<Int32, Offset<UInt32>> __cacheUInt = new ConcurrentDictionary<Int32, Offset<UInt32>>(); | ||
| ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>(); | ||
| ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>(); |
There was a problem hiding this comment.
Field '__cacheLong' can be 'readonly'.
| ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>(); | |
| readonly ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>(); |
| ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>(); | ||
| ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>(); | ||
| //ConcurrentDictionary<Int32, Offset<UInt64>> __cacheULong = new ConcurrentDictionary<Int32, Offset<UInt64>>(); | ||
| ConcurrentDictionary<Int32, Offset<Double>> __cacheDouble = new ConcurrentDictionary<Int32, Offset<Double>>(); |
There was a problem hiding this comment.
Field '__cacheDouble' can be 'readonly'.
| ConcurrentDictionary<Int32, Offset<Double>> __cacheDouble = new ConcurrentDictionary<Int32, Offset<Double>>(); | |
| private readonly ConcurrentDictionary<Int32, Offset<Double>> __cacheDouble = new ConcurrentDictionary<Int32, Offset<Double>>(); |
| ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>(); | ||
| //ConcurrentDictionary<Int32, Offset<UInt64>> __cacheULong = new ConcurrentDictionary<Int32, Offset<UInt64>>(); | ||
| ConcurrentDictionary<Int32, Offset<Double>> __cacheDouble = new ConcurrentDictionary<Int32, Offset<Double>>(); | ||
| ConcurrentDictionary<Int32, Offset<String>> __cacheString = new ConcurrentDictionary<Int32, Offset<String>>(); |
There was a problem hiding this comment.
Field '__cacheString' can be 'readonly'.
| ConcurrentDictionary<Int32, Offset<String>> __cacheString = new ConcurrentDictionary<Int32, Offset<String>>(); | |
| readonly ConcurrentDictionary<Int32, Offset<String>> __cacheString = new ConcurrentDictionary<Int32, Offset<String>>(); |
There was a problem hiding this comment.
@DocMoebiuz - The FSUIPC Client DLL action (This property can be used to force this offset to be read or written on the next process) is documented at http://fsuipc.paulhenty.com/Reference/html/classFSUIPC_1_1Offset.html. Paul suggested different group names for read v write Offsets however this would require Mobiflight to segregate usage of FSUIPC offsets. The work-around was to set the Action at next process.
I'm new to modifying code within open source projects. Is there any documentation/advice on how to perform CoPilot code reviews and also why refactoring is considered a better way ? Usually the first action of anyone is a source code compare to understand changes and any refactoring would result in a mass of such notifications ! Perhaps refactoring should be a 2nd step process after initial code supply ?
This PR has been tested in my environment, the commented out debug(s) assisted in proving the issue and that locking resolved it before being commented out. I can add additional comments as to their purpose but again it helps on any future fixes what standards any developers should be adhering to.
Thanks again for all your support
Gary
|
@ghingres will you be able to check the comments and copilot feedback and implement them? |
Fix bug #2651 (FSUIPC Integration is not thread safe) by changing the following