Page MenuHomePhabricator

Implement GlobalDbSingleton API for both platforms
ClosedPublic

Authored by marcin on Sep 27 2022, 4:49 AM.
Tags
None
Referenced Files
F3373940: D5233.id17410.diff
Tue, Nov 26, 12:45 PM
F3373126: D5233.id17401.diff
Tue, Nov 26, 8:26 AM
F3372728: D5233.id17396.diff
Tue, Nov 26, 7:35 AM
F3372717: D5233.id17127.diff
Tue, Nov 26, 7:32 AM
F3372665: D5233.id17335.diff
Tue, Nov 26, 7:21 AM
F3370564: D5233.id17396.diff
Tue, Nov 26, 3:24 AM
F3370563: D5233.id17341.diff
Tue, Nov 26, 3:24 AM
F3370562: D5233.id17335.diff
Tue, Nov 26, 3:24 AM

Details

Summary

Implements GlobalDBSingleton API for both platforms. On iOS we use main queue and atomic boolean for synchronization. On Android we enable multithreading from the begining.

Test Plan

Spawn multiple threads in AppDelegate (for instance multiple instances of WorkerThread) and schedule database tasks from them using GlobalDBSingletonIOSProxy API. Do it before and after CommCoreModule instantiation. No crash should happen.

Diff Detail

Repository
rCOMM Comm
Branch
marcin/db-singleton
Lint
No Lint Coverage
Unit
No Test Coverage

Event Timeline

jon requested changes to this revision.Sep 27 2022, 9:18 AM
jon added a subscriber: jon.
jon added inline comments.
native/ios/Comm/GlobalDBSingletonIOSProxy.mm
13

is there a reason we can't use dispatch_async? I'm assuming we don't have a hard timeline for tasks.

This revision now requires changes to proceed.Sep 27 2022, 9:18 AM
native/ios/Comm/GlobalDBSingletonIOSProxy.mm
13

You are right. scheduleOrRun method signature suggests that we do not make guarantees about the time a task will be executed. If someone is interested in waiting for the task completion they can put a promise inside a task and wait on it in a thread that is calling this method.

tomek requested changes to this revision.Sep 28 2022, 3:41 AM

The code looks rather ok - only not sure about enableMultithreading safety.

Additional note: I was wondering if this code structure can be improved by taking a similar approach to what we do e.g. for Logger. We have a common interface there with platform-specific implementations. I guess it might be a big improvement, because currently it would be hard to call GlobalDBSingletonIOSProxy on iOS from C++ code.

native/ios/Comm/GlobalDBSingletonIOSProxy.mm
19–26 ↗(On Diff #17127)

What happens when we have a task scheduled using scheduleOrRun on dispatch_get_main_queue and then main thread calls enableMultithreading before the queue started processing the task?

This revision now requires changes to proceed.Sep 28 2022, 3:41 AM
In D5233#154499, @tomek wrote:

The code looks rather ok - only not sure about enableMultithreading safety.

Additional note: I was wondering if this code structure can be improved by taking a similar approach to what we do e.g. for Logger. We have a common interface there with platform-specific implementations. I guess it might be a big improvement, because currently it would be hard to call GlobalDBSingletonIOSProxy on iOS from C++ code.

I get your point and can surely implement that. But is it likely that we call this class from C++? We generally try to avoid platform depended code in CommonCpp. The only place in CommonCpp we call GlobalDBSingleton is CommCoreModule which does not need synchronization implemented by this class.

native/ios/Comm/GlobalDBSingletonIOSProxy.mm
19–26 ↗(On Diff #17127)

This is one thread so enableMultithreading completes and when block with scheduleOrRun starts being executed the task will be run on dedicated database thread. I found an article https://www.objc.io/issues/2-concurrency/low-level-concurrency-apis/. It has a sentence: " If you're submitting a block to GCD, and that code blocks the thread, this thread is no longer available at that point in time to do other work -- it's blocked". This leads to conclusion that blocks submitted to the queue are executed atomically. So it is not possible that queue starts processing block with scheduleOrRun, comes to a point it wants to call task() synchronously (since multithreading was not enabled), then stops and goes to a code that enables multithreading and comes back to executing task at the main thread afterwards.

In D5233#154499, @tomek wrote:

The code looks rather ok - only not sure about enableMultithreading safety.

Additional note: I was wondering if this code structure can be improved by taking a similar approach to what we do e.g. for Logger. We have a common interface there with platform-specific implementations. I guess it might be a big improvement, because currently it would be hard to call GlobalDBSingletonIOSProxy on iOS from C++ code.

After considering this idea for a while I came to the conclusion that we already do so for Android. We define JNI helper classes in headers in CommonCpp and implement them in Android specific directory. We can do the same for iOS - define iOS proxy class in header in CommonCpp and implement it in Objective C++ file in iOS specific directory.

Define in CommonCpp, implement in iOS.

We should be really sure about this code. If there are any issues, it will probably cause crashes we don't know about. I encourage everybody on this diff to be extremely thorough. We should be 100% sure of every line here. We should not be guessing, or trying things that seem to work without understanding 100% of why/how they work.

tomek requested changes to this revision.Oct 3 2022, 6:11 AM
In D5233#154499, @tomek wrote:

The code looks rather ok - only not sure about enableMultithreading safety.

Additional note: I was wondering if this code structure can be improved by taking a similar approach to what we do e.g. for Logger. We have a common interface there with platform-specific implementations. I guess it might be a big improvement, because currently it would be hard to call GlobalDBSingletonIOSProxy on iOS from C++ code.

I get your point and can surely implement that. But is it likely that we call this class from C++? We generally try to avoid platform depended code in CommonCpp. The only place in CommonCpp we call GlobalDBSingleton is CommCoreModule which does not need synchronization implemented by this class.

Why it doesn't need this synchronization? Is calling CommCoreModule methods from App Delegate safe? I think that the safest approach would be to call methods on GlobalDBSingleton in both App Delegate and CommCoreModule. These methods would have different implementation for different platforms. This approach would reduce probability of mistakes. Can you see any disadvantages?

native/ios/Comm/GlobalDBSingletonIOSProxy.mm
19–26 ↗(On Diff #17127)

Ok, this makes sense. I tried to find any issue in the solution, but it seems to be quite solid. Basically, every operation that involves using GlobalDBSingleton happens on main thread. That allows doing the operations without any synchronization.

The only thing which we have to ensure is that CommCoreModule also uses GlobalDBSingleton from main thread. I think that if we don't do that, there might be some threading bugs.

This revision now requires changes to proceed.Oct 3 2022, 6:11 AM

Why it doesn't need this synchronization? Is calling CommCoreModule methods from App Delegate safe?

We don't call CommCoreModule from AppDelegate. I was talking about using GlobalDBSingleton methods in CommCoreModule. The reason it safe is that we enable multithreading in CommCoreModule constructor.

I think that the safest approach would be to call methods on GlobalDBSingleton in both App Delegate and CommCoreModule. These methods would have different implementation for different platforms. This approach would reduce probability of mistakes. Can you see any disadvantages?

I agree, but my initial thought was to have a common interface for both platforms. But since we end up implementing additional synchronization layers then perhaps it is better to define header in CommonCpp and implement independently for Android and iOS.

marcin retitled this revision from Implement GlobalDbSingleton proxy to be used in iOS specific native code to Implement GlobalDbSingleton API for both platforms.Oct 5 2022, 2:58 AM
marcin edited the summary of this revision. (Show Details)

I'll defer to tomek for final yes. But I don't see anything wrong

tomek added inline comments.
native/ios/Comm/GlobalDBSingleton.mm
12–19 ↗(On Diff #17341)

Could you remind me why we're scheduling only from the main thread? I think that currently the code is correct, but it might be possible to simplify it.

Does introducing a shared atomic pointer solve the causes?

This revision is now accepted and ready to land.Oct 6 2022, 6:52 AM
native/ios/Comm/GlobalDBSingleton.mm
12–19 ↗(On Diff #17341)
  1. If there are two threads trying to access the database before enableMultithreading is called we need either to synchronize with mutexes or to schedule on the main threads queue. I opted for the second idea since it sounds safer to me than blocking threads since it is likely that one of them is a main thread. For this reason we might want to keep dispatching on the main thread in scheduleOrRun
  1. Imagine the following case: main thread calls scheduleOrRun before enableMultithreading is called, it reads databaseThread atomically as nullptr so it starts to execute a database task. Assume this task is quite lengthy. Now there is another thread that calls enableMultithreading and the other one that calls scheduleOrRun. Since multithreading was enabled the second task will start to execute on dedicated database thread while there is still some database operation going on on the main thread. The app crashes. Introduction of atomic shared pointer solves the case when there are multiple threads calling enableMultithreading at once but it does not help if there are multiple threads calling enableMultithreading and scheduleOrRun at once. I understand that this scenario is not highly probable but still possible. Application would probably work but it would be a race condition.

We could create another atomic boolean that indicates whether there is ongoing database operation that started its execution before enableMultithreading and schedule enableMultithreading execution on the main thread only if it is true. This solution is more general but also more complicated so I would personally defer its implementation until we really need it.

Moreover I have probably been to optimistic regarding atomic shared pointer during my private discussion with @tomek . From the docs it looks like they are C++20 feature (https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic2). I will try to compile the code to see if we can use it the same way we use if constexpr which is a C++17 feature.

native/ios/Comm/GlobalDBSingleton.mm
12–19 ↗(On Diff #17341)

I was unable to compile std::atomic<std::shared_ptr<>> here.

We must use sync since we call this from CommCoreModule now

We must use sync since we call this from CommCoreModule now

Can you provide more context on this last-minute change?

We must use sync since we call this from CommCoreModule now

Can you provide more context on this last-minute change?

Unfortunately it was a mistake (not a harmful one) I made being under pressure. I was looking at the entire code once again before landing and created a scenario in my mind that some thread calls enableMultithreading , then escapes from a function before a task completes (it is an asynchronous dispatch). I thought that another thread might then schedule a task before a database thread is created so I decided to make it synchronous. This is ridiculous since enableMultithreadingCommonImpl sets atomic boolean flag only after database thread is created. Before this flag is set every task will execute on main thread. Only after it is set tasks will being to execute on dedicated database thread. I was under pressure and my thinking was not clear. This change was unnecessary but not harmful. I will create a follow-up task for database singleton issue (mainly to resolve whether we can use atomic shared pointers as Tomek suggested) and I will make sure that it has a subtask to make this call asynchronous back. I will also make sure release build that includes this stack has asynchronous call here.