Couchbase Lite Swift: Replicator Fails to Write to Websocket in Background?

I have a very tiny utility macOS app that I built to migrate data from MongoDB to Couchbase. To do this, I use the Couchbase Lite Swift SDK. I create about 5 million documents locally (from some JSON files), then start a replicator to push them to Capella.

I have noticed odd behavior. If I minimize the app while sync is running or allow the window to become occluded (hidden behind other apps), after a few minutes I start to see errors about unwritable websockets. At first, there are only a few. But after a while, the console is mostly errors:

If I bring the app’s window back to the front, this instantly resolves and the websocket errors vanish:

Has anyone encountered something similar? It takes a few minutes for this to occur, so you have to be syncing a large amount of data.

Code

Here’s how I’m running the Replicator:

func startReplicator()
    {
        var collections: [Collection] = []
        for collectionName: String in collectionNames {
            let c: Collection = try! database.createCollection(name: collectionName, scope: scopeName)
            collections.append(c)
        }
        
        var config = ReplicatorConfiguration(target: syncEndpoint)
        config.continuous = true
        config.replicatorType = .pushAndPull
        config.addCollections(collections)
        config.authenticator = BasicAuthenticator(username: syncEndpointUsername, password: syncEndpointPassword)
        replicator = Replicator(config: config)
        replicatorStatusToken = replicator!.addChangeListener({ change in
            let status: Replicator.Status = change.status
            
            switch status.activity
            {
            case .stopped:
                logger.info("Replicator stopped")
                
            case .offline:
                logger.info("Replicator offline")
                
            case .connecting:
                logger.info("Replicator connecting...")
                
            case .idle:
                logger.info("Replicator is idle")
                
            case .busy:
                if status.progress.total != 0 {
                    let portion: Double = Double(status.progress.completed) / Double(status.progress.total)
                    logger.info("Replicator is syncing: \(portion)")
                } else {
                    logger.info("Replicator is syncing...")
                }
                
            @unknown default:
                logger.warning("Unhandled Replicator.Status.ActivityLevel case: \(status.activity.rawValue, privacy: .public). Setting ModelContext syncStatus to idle.")
            }
            
            if let error = change.status.error as? NSError {
                logger.error("Sync replication error: \(error.localizedDescription, privacy: .public)")
            }
        })
        replicator!.start(reset: false)
        replicatorIsLaunched = true
    }

That is expected behaviour. I think you are looking for allowReplicatingInBackground in ReplicatorConfiguration. This is false by default. (and it is only for iOS)

This isn’t an iOS app.

Looking at the code, I believe this warning is harmless and shouldn’t actually be logged. (This code path used to be silent, but a warning was added later on.)

What’s going on is that our RPC code wants to write to the WebSocket but is being throttled because the TCP socket’s buffer is full. This is harmless because the code will be notified when the socket becomes writeable, and will retry the write.

My guess is that the reason this occurs after you hide the app is because the OS is reducing its available network bandwidth, so the socket is now unable to keep up with the rate at which the process is writing. I don’t know anything specific about such OS tuning behaviors; maybe they’re documented somewhere and there’s a way to bypass them. But again, all this means is that your app is slowing down slightly; there’s no data loss or anything.

1 Like

I’ve missed the part you mentioned is a macOS app, sorry.

If the replication itself takes a few minutes, can you please check if App Nap gets activated for the app in question during that time? That would explain what Jens just mentioned.

1 Like