0

I have this script which change all passwords of existing FireBase Authentication users that we use for testing purposes in order to reset users to a "clean" state.

var admin = require('firebase-admin');

// Input args
var jsonKey = process.argv[2];
var projectId = process.argv[3];
var newPassword = process.argv[4];

var serviceAccount = require(jsonKey);

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    databaseURL: "https://" + projectId + ".firebaseio.com"
});

// FIXME Only for debug purposes
var index = 1;

/**
* https://firebase.google.com/docs/auth/admin/manage-users#update_a_user
*/
function nextUser(uid, timeout) { 
    setTimeout(() => {
        admin.auth().updateUser(uid, { 
            password : newPassword
        })
        .then(function() {
            console.log(index++ + ') Successfully updated user', uid);
        })
        .catch(function(error) {
            console.log(index++ + ') Error updating user:', error);
        });
    }, timeout);
}

/**
* https://firebase.google.com/docs/auth/admin/manage-users#list_all_users
* https://stackoverflow.com/a/58028558
*/
function listAllUsers(nextPageToken) {
    let timeout = 0;
    admin.auth().listUsers(1000, nextPageToken)
        .then(function (listUsersResult) {
            console.log("Retrieved " + listUsersResult.users.length + " users");

            listUsersResult.users.forEach(function (userRecord) {
                timeout = timeout + 100
                nextUser(userRecord.uid, timeout)
            });

            if (listUsersResult.pageToken) {
                listAllUsers(listUsersResult.pageToken);
            }
        })
        .catch(function (error) {
            console.log('Error listing users:', error);
        });
}

listAllUsers();

The script is launched from a local machine with

node <filename.js> <path of jsonkey> <projectid> <new password>

As you can see the iterator is running with timeouts, this is needed after I encountered a problem with "Exceeded quota for updating account information" which I tried to solve with this answer

The script is running fine, all users are processed (I added a sequential counter only for this kind of debug) correctly and not "quota" error is thrown. The problem is that at the end of the script the shell prompt does not exit, the process remains hanging and I need a CTRL+C to kill it. Besides the annoying part of this behaviour, this is blocking a much wider script which call this one and does not proceed to next line of commands.

It seems that after all setTimeout function has been executed, the main process is not automatically shutting down.

How can I solve this?


EDIT

I tried different approaches:

The second version works, but it is very slow. So I started from initial script and I added a "monitor" which wait for all users to be completed (with a very dirty counter) and close the script at the end

var admin = require('firebase-admin');

// Input args
var jsonKey = process.argv[2];
var projectId = process.argv[3];
var newPassword = process.argv[4];

var serviceAccount = require(jsonKey);

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    databaseURL: "https://" + projectId + ".firebaseio.com"
});

// Index to count all users that have been queued for processing
var queued = 0;

// Index to count all user that have completed the task
var completed = 0;

/**
* https://firebase.google.com/docs/auth/admin/manage-users#update_a_user
*/
function nextUser(uid, timeout) { 
    setTimeout(() => {

        /* ------ Process User Implementation ------ */
        admin.auth().updateUser(uid, { 
            password : newPassword
        })
        .then(function() {
            console.log(++completed + ') Successfully updated user ', uid);
        })
        .catch(function(error) {
            console.log(++completed + ') Error updating user:', error);
        });
        /* ------ Process User Implementation ------ */

    }, timeout);

    queued++;
}

/**
* https://firebase.google.com/docs/auth/admin/manage-users#list_all_users
* https://stackoverflow.com/a/58028558
*/
function listAllUsers(nextPageToken) {
    let timeout = 0;
    admin.auth().listUsers(1000, nextPageToken)
        .then(function (listUsersResult) {
            console.log("Retrieved " + listUsersResult.users.length + " users");

            listUsersResult.users.forEach(function (userRecord) {
                timeout = timeout + 100
                nextUser(userRecord.uid, timeout)
            });

            if (listUsersResult.pageToken) {
                listAllUsers(listUsersResult.pageToken);
            } else {
                waitForEnd();
            }
        })
        .catch(function (error) {
            console.log('Error listing users:', error);
        });
}

function waitForEnd() {
    if(completed < queued) {
        console.log("> " + completed + "/" + queued + " completed, waiting...");
        setTimeout(waitForEnd, 5000);
    } else {
        console.log("> " + completed + "/" + queued + " completed, exiting...");
        admin.app().delete();
    }
}

listAllUsers();

It works, it's rapid. Enough for me as it is a testing script

Deviling Master
  • 3,033
  • 5
  • 34
  • 59
  • I don't know Firebase, but is there some way you need to close a database connection? The fact that node.js doesn't exit automatically when you think your code is done means that some network connection, timer or other asynchronous operation is still open or pending. All I can see that could be that in your app is perhaps an open database connection. – jfriend00 Nov 13 '19 at 19:05
  • See this [How to close firebase connection in node.js](https://stackoverflow.com/questions/38222757/how-to-close-firebase-connection-in-node-js). – jfriend00 Nov 13 '19 at 19:06
  • I think this will be easier for yourself if you use async/await syntax along with the standard "[sleep](https://stackoverflow.com/a/39914235)" mechanism to space out the calls. The, when your loop is done, you can call admin.app().delete() to shut down the admin SDK after the last update completes. A standard loop will also prevent a possible out of memory situation from kicking off too many of these delayed calls at the same time. – Doug Stevenson Nov 13 '19 at 19:19
  • @DougStevenson I tried as you suggested an async/await, this is the current code: https://repl.it/repls/RoundedAnchoredDos I tried many times but I'm unable to find a fully working solution. First I had to remove all inner methods in order to respect the sleep (now all the code is in the main method). The problem now that `admin.app().delete();` is called before all users has been processed – Deviling Master Nov 14 '19 at 08:57
  • @DougStevenson this is a more "synchronous" versions, it does not need a sleep as all the calls are chained: https://repl.it/repls/DelightfulUnfoldedHexagon Of course now is very slow because only 1 call per time is made. But at least it works, the process correctly ends when users are completed. – Deviling Master Nov 14 '19 at 16:05
  • The whole point of the rate limiting on the auth API is to force clients to slow down in order to avoid abuse of the system. No matter how you stagger your calls to that API, wether settimeout with varying delays, or await, it's going to be slow. – Doug Stevenson Nov 14 '19 at 16:52
  • No argue with that, my point was that with the last version of the script (no timeout, it simply made one request at a time), we are much slower compared to previous version. There is a "speed window" that you can increase up to be near max api-rate. With the one-request version I think that the speed is way lower that the actula maxium we can reach. If API supports max 10 op/s, a parallel processing is possible, as long as we stay inside that rate. – Deviling Master Nov 14 '19 at 18:07
  • @DougStevenson I edited the main question with an updated code from the first version. I think this is the best I can reach with actual implementation. – Deviling Master Nov 15 '19 at 19:13

1 Answers1

0

Try to put the following exit code after in setTimeout function.

process.exit() // exit

I updated the code.

function listAllUsers(nextPageToken) {
    let timeout = 0;
    admin.auth().listUsers(1000, nextPageToken)
        .then(function (listUsersResult) {
            console.log("Retrieved " + listUsersResult.users.length + " users");

            listUsersResult.users.forEach(function (userRecord) {
                timeout = timeout + 100
                nextUser(userRecord.uid, timeout)
            });

            if (listUsersResult.pageToken) {
                listAllUsers(listUsersResult.pageToken);
            }
            process.exit(0); // exit script with success
        })
        .catch(function (error) {
            console.log('Error listing users:', error);
            process.exit(1); // exit script with failure
        });
}

This function will exit Node.js script.

process.exit([code]) Ends the process with the specified code. If omitted, exit uses the 'success' code 0.

To exit with a 'failure' code:

process.exit(1);

The shell that executed node should see the exit code as 1.

Official doc: https://nodejs.org/api/process.html#process_exit_codes

Rise
  • 1,493
  • 9
  • 17
  • That's not going to work, since the code is kicking off lots of calls to setTimeout on staggered delay. Only after the very last one complete should the script terminate. – Doug Stevenson Nov 13 '19 at 19:20
  • Yes.. but this is a way to exit script programmatically. if there is anything that block process.exit(), then it may have another issue, I think. – Rise Nov 13 '19 at 19:28
  • I just updated listAllUsers() function. it's not nextUser function. Hope that works for you) – Rise Nov 13 '19 at 19:29
  • 1
    `process.exit()` will indeed terminate the process immediately. Unfortunately, this code still doesn't work because it will exit before any of the users have been updated. All the APIs are asynchronous and return immediately with a promise that indicates completion. – Doug Stevenson Nov 13 '19 at 20:38