0

I have a function to adding player like so:

inside my external js (player.js)

module.exports.player = function(appPlayer, db){

appPlayer.post('/', async(req, res) => {
            const data = req.body
            // data
            const ipAdress = data.ipAdress
            const carslidekey = data.id
            const nickname = data.nickname
            // random color for 
            const randomCol = Math.floor(Math.random()*16777215).toString(16)
    
            let player = {
                ipAdress: ipAdress,
                nickname: nickname,
                color: `#${randomCol}`
            }
    
            await db.collection('kartesian').doc(carslidekey)
                    .collection('player')
                    .doc(ipAdress)
                    .set(player).then(() => {
                        let dataResponse = {
                            status: true,
                            idDoc: ipAdress,
                            ipAdress: ipAdress,
                            color: `#${randomCol}`
                        }
                        const jsonStr_1 = JSON.stringify(dataResponse)
                        res.send(jsonStr_1)
                        
                    }).catch((err) => {
                        let dataResponse = {
                            status: false,
                            idDoc: ipAdress,
                            ipAdress: ipAdress,
                            msg: `Terjadi kesalahan ${err.data}`
                        }
                        const jsonStr_1 = JSON.stringify(dataResponse)
                        res.send(jsonStr_1)
                    })
            
        })
}

and my index.js (cloud functions) i wrote https request like below:

    ......
    
    const playerHandler = require('./src/player')
    playerHandler.player(app, db)
    exports.player = functions.https.onRequest(app)

    ......

My problem is sometime this function called by different devices and might be concurrently, even though I've created a method set with a different id but why sometimes I find the previous documentation replaced by the document after. how to ensure that there is no overlap? Thanks

Nanda Z
  • 1,604
  • 4
  • 15
  • 37
  • If I understand well, you can process different request in parallel, but for an ID, the order is important, correct? In addition, is it real time (the requester wait for the answer) or async (the requester call the function and doesn't care the answer (fire and forget))? – guillaume blaquiere Mar 18 '22 at 19:27
  • This seems like some erratic behavior caused by some not-so-obvious reasons. I would go for background activities or temporary files; both are covered in [this guide](https://cloud.google.com/functions/docs/bestpractices/tips); although I would check all the points just to be sure. – Alex Mar 18 '22 at 22:37
  • @guillaumeblaquiere Id is come from ipAddress, so no need order id actually. Yes, this is real time, requester doesn't care the response – Nanda Z Mar 19 '22 at 00:38
  • @Alex today, i got this issue so often. i use `"firebase-functions": "^3.18.1"` if you need more detail i can provide more snippet code – Nanda Z Mar 19 '22 at 00:48

1 Answers1

1

Option 1:

I don't think there is a way besides reading the document as a pre-check before writing. I would recommend implementing a queue -> the cloud function would push to the queue, and a separate cloud function would read the queue and execute on it synchronously.

Also, you're mixing await and promises (e.g. .then), you do not need to do that, see code below.

For example:

Note: I didn't run this code, but it should be close to working

Note2: @Nanda Z made a good point, it's probably not a good idea to run background functions without implementing them as cloud tasks and cloud events

let playerQueue = []
var isProcessingQueue = false

async function handlePlayerQueue(appPlayer, db) {
    if (isProcessingQueue) return
    isProcessingQueue = true

    if (playerQueue.length == 0) return

    // remove the first item from the queue
    player = playerQueue.shift()

    let ref = db.collection('kartesian').doc(player.carslidekey)
                    .collection('player')
                    .doc(player.ipAdress)

    // Check if player exists

    var playerExists = false

    try {

        playerExists = await ref.get().exists

    } catch (err) {

        console.error("Could not get player record", err)

    }

    // exit early
    if (playerExists) {

        isProcessingQueue = false

        // recursively handle queue
        handlePlayerQueue(appPlayer, db)
        
        return
    }


    // wrap await in try...catch to catch exceptions.
    try {
        let setResponse = await ref.set(player)
        let dataResponse = {
            status: true,
            idDoc: player.ipAdress,
            ...player
        }
        const jsonStr_1 = JSON.stringify(dataResponse)
        console.log(`Player data pushed: ${jsonStr_1}`)

    } catch(err) {
        let dataResponse = {
            status: false,
            idDoc: player.ipAdress,
            ...player
            msg: `Terjadi kesalahan ${err.data}`
        }
        const jsonStr_1 = JSON.stringify(dataResponse)
        console.error(`Failed to set player: ${jsonStr_1}`)
    }

    isProcessingQueue = false

    // recursively handle queue
    handlePlayerQueue(appPlayer, db)
}

module.exports.player = function(appPlayer, db){
appPlayer.post('/', async(req, res) => {

    const data = req.body
    // data
    const ipAdress = data.ipAdress
    const carslidekey = data.id
    const nickname = data.nickname
    // random color for 
    const randomCol = Math.floor(Math.random()*16777215).toString(16)

    let player = {
        carslidekey, // need to add this so that the queueHandler can use it
        ipAdress: ipAdress,
        nickname: nickname,
        color: `#${randomCol}`
    }

    // if javascript wasn't singlethreaded, this is where there'd be a lock =)
    playerQueue.push(player)

    // call the queue handler
    handlePlayerQueue(appPlayer, db)

    // Tell the client we received the request, they won't know the status, but you can set up another endpoint to check if the record exists
    res.send({message: "Request received and queue\'d!"})
    
})
}

Option 2:

You could instead add a Firestore rule to not allow writing to a document if it's already populated:

match /kartesian/{carslidekey}/player/{player} {
    // you can use the `resource.data` variable to access the player document:
    // resource will be null if the player doesn't exist. resource.data is overkill but I put it there just fyi 
    allow write: if request.auth != null && (resource == null || resource.data == null);
}

I tested this rule to make sure it behaves as expected. It should let you write only if you're authenticated AND the resource is empty. So writes that come in later will fail.


It's important to note that rules only apply for online writing and will not be applied to offline/cached data.

sleepystar96
  • 721
  • 3
  • 12
  • why should be recursive? is that okay in cloud function – Nanda Z Mar 19 '22 at 02:08
  • I made a mistake, it's not best practice to run background functions. You're right, best practice would be to set up cloud events instead: https://cloud.google.com/functions/docs/tutorials/pubsub – sleepystar96 Mar 19 '22 at 02:30
  • and cloud tasks: https://cloud.google.com/tasks/docs/creating-queues – sleepystar96 Mar 19 '22 at 02:32
  • but the overall implementation should be similar. – sleepystar96 Mar 19 '22 at 02:32
  • for second option, actually i use Admin SDK. admin does write in player collection. But my case is sometime admin override previous player document. I just thinking that admin have not finished yet and not keeping wrote another call. I wish there is something kind transaction process – Nanda Z Mar 19 '22 at 02:42
  • for first option i don't get the `player` var comes from – Nanda Z Mar 19 '22 at 02:46
  • why not use client sdk and write straight to the collection? – sleepystar96 Mar 19 '22 at 03:58
  • Create a task and put the player var in the payload? https://cloud.google.com/tasks/docs/creating-http-target-tasks#creating_http_target_tasks – sleepystar96 Mar 19 '22 at 04:02
  • in my project, we do not push user to register. We just provide what they gonna do with some options and that tasks perform by admin sdk – Nanda Z Mar 19 '22 at 04:03
  • I see, have you considered anonymous authentication? https://firebase.google.com/docs/auth/web/anonymous-auth – sleepystar96 Mar 19 '22 at 04:21
  • Not yet, does anonymous account has privilege to write documents? – Nanda Z Mar 19 '22 at 04:31
  • Yes, an anonymous signed-in client will have {"sign_in_provider": "anonymous"} as the data in `request.auth`. So the rules in option 2 above will work to check that they are signed in. – sleepystar96 Mar 19 '22 at 04:39
  • btw, I just tested the rules in Option 2, they work as expected – sleepystar96 Mar 19 '22 at 04:40
  • i read this https://stackoverflow.com/questions/38694015/what-happens-to-firebase-anonymous-users and anonymous user are not expired. I need to purge them – Nanda Z Mar 19 '22 at 04:41
  • Sorry to ask, but why does expiring matter in this context? – sleepystar96 Mar 19 '22 at 04:43
  • My users might be domination by children, they might be forgot to close. It is possible to admin sdk to clean that? – Nanda Z Mar 19 '22 at 04:45
  • Of course, also as long as they don't log out and the token doesn't expire, they should have the same sign in token. Assuming this is a mobile app. If this is a client side web-app, then you can clean up after some time has passed using the admin sdk doesn't sound like a bad idea – sleepystar96 Mar 19 '22 at 04:47
  • 1
    I see, thank you bro.. i think i try that as alternative solution. – Nanda Z Mar 19 '22 at 04:50
  • np, let me know if you need any help! – sleepystar96 Mar 19 '22 at 04:52