Использование асинхронного ожидания в рекурсивной функции с помощью process.nextTick()

У меня есть работающая рекурсивная функция в приложении node.js, которое использует Promises с обратным вызовом process.nextTick(). Мне любопытно, как это будет/может работать с асинхронным ожиданием.

Я пробовал несколько разных вещей, но независимо от того, что я сделал, асинхронная функция возвращается к вызывающей функции до завершения всех обратных вызовов nextTick.

не работает (удалить кеш, вызванный из экспресс-маршрута)

const deleteCache = async () => {
  try {
    const cacheRef = fsDb.collection('Cache');
    return await deleteDocsBatch(cacheRef, 30);
  } catch (e) {
    console.error('error in deleteCache:' + e);
  }
};

const deleteDocsBatch = async (cacheRef, batchSize) => {
  try {
    // get all the cached docs, limit to 30 to avoid potential memory issues
    const snapShot = await cacheRef.limit(batchSize).get();
    if (snapShot.size === 0) { return; }

    const batch = fsDb.batch();
    snapShot.docs.forEach((doc) => {
      batch.delete(doc.ref);
    });

    await batch.commit();
    process.nextTick(() => {
      deleteDocsBatch(cacheRef, batchSize);
    });
  } catch (e) {
    console.error('error in deleteDocsBatch:' + e);
  }
};

работающий:

function deleteCollection (batchSize) {
  var collectionRef = fsDb.collection('Cache');
  var query = collectionRef.orderBy('__name__').limit(batchSize);

  return new Promise((resolve, reject) => {
    deleteQueryBatch(fsDb, query, batchSize, resolve, reject);
  });
}

function deleteQueryBatch (db, query, batchSize, resolve, reject) {
  query.get()
    .then((snapshot) => {
      // When there are no documents left, we are done
      if (snapshot.size === 0) {
        return new Promise((resolve, reject) => { resolve(0); });
      }

      // Delete documents in a batch
      var batch = db.batch();
      snapshot.docs.forEach((doc) => {
        batch.delete(doc.ref);
      });

      return new Promise((resolve, reject) => {
        batch.commit().then(() => {
          resolve(snapshot.size);
        })
          .catch(reject);
      });
    }).then((numDeleted) => {
      if (numDeleted === 0) {
        resolve();
        return;
      }

      // Recurse on the next process tick, to avoid
      // exploding the stack.
      process.nextTick(() => {
        deleteQueryBatch(db, query, batchSize, resolve, reject);
      });
    })
    .catch(reject);
}

Можно ли написать эту рекурсивную функцию с помощью nexttick(), используя асинхронное ожидание?

исходный образец кода firestore:

https://firebase.google.com/docs/firestore/manage-data/delete-data


person Rich Williams    schedule 17.04.2019    source источник


Ответы (1)


Итак, воспользуйтесь тем фактом, что когда вы прикрепляете что-либо к обещанию через .then(), оно будет выполнено в следующем тике. Другими словами, ваш process.nextTick даже не нужен в исходном коде. В худшем случае вы войдете в рекурсивный вызов, но немедленно выйдете. Никогда не выходит за пределы глубины 1.

А await — это синтаксический сахар для .then(). Под капотом код все равно трансформируется в серию .then(). Так это

await deleteDocsBatch(cacheRef, batchSize);

вместо

process.nextTick(() => {
  deleteDocsBatch(cacheRef, batchSize);
});

должно быть достаточно. Начальная синхронная часть (т.е. до первого ожидания) функции будет выполняться рекурсивно, хотя, как я уже упоминал. Поэтому, если вы действительно хотите быть уверены, вы можете принудительно установить асинхронность через

await Promise.resolve();
await deleteDocsBatch(cacheRef, batchSize);

Суть в том, что вы говорите интерпретатору: «Эй, это точка асинхронности, иди займись чем-нибудь другим, типа ничего, хорошо?».

Также обратите внимание, что await new Promise(res => process.nextTick(res)); является альтернативой. Хотя перебор.


Пример:

async function p1() {
    console.log('interrupt');
};

async function p2() {
    console.log('1');
    await Promise.resolve();
    console.log('2');
};

p2();
p1();

Итак, как вы можете видеть, обе функции на самом деле синхронны. За исключением того, что p2 не потому, что внутри него await. И этот await переводит все, что ниже, на следующий тик, позволяя p1 работать между ними. Функция p2 эквивалентна

function p2() {
    console.log('1');
    return Promise.resolve().then(() => {
        console.log('2');
    });
};

и вывод:

1
interrupt
2

И еще один пример. Это очень быстро превышает максимальную глубину рекурсии:

async function go(i)
{
  console.log(i);
  go(i+1);
}

go(0);

Это не так. Всегда.

async function go(i)
{
  console.log(i);
  await Promise.resolve();
  go(i+1);
}

go(0);

Второй код фактически использует постоянный объем памяти.


Вывод: любой (доступный) await внутри функции нарушит рекурсивный вызов. И вот как может выглядеть код:

const deleteDocsBatch = async (cacheRef, batchSize) => {
  try {
    // get all the cached docs, limit to 30 to avoid potential memory issues
    const snapShot = await cacheRef.limit(batchSize).get();
    if (snapShot.size === 0) { return; }

    const batch = fsDb.batch();
    snapShot.docs.forEach((doc) => {
      batch.delete(doc.ref);
    });

    await batch.commit();
    await deleteDocsBatch(cacheRef, batchSize);
  } catch (e) {
    console.error('error in deleteDocsBatch:' + e);
  }
};

и единственная рекурсивная проблема, о которой вам следует беспокоиться, - это условие остановки (например: бывают ли случаи, когда эта функция никогда не завершается?). Этот код не съест вашу память.

person freakish    schedule 17.04.2019
comment
Это не меня минусовали. Я попробовал await Promise.resolve(); и «излишнее убийство», и ни то, ни другое не дало желаемого эффекта. В обоих случаях некоторые nexticks запускались после того, как родительская функция возвращалась вызывающей стороне. - person Rich Williams; 18.04.2019
comment
@RichWilliams Я добавил полный код в конце. Вы говорите, что этот код производит какое-то выполнение в фоновом режиме после выхода? Звучит маловероятно. Разве batch.delete() не является асинхронным? (я не знаю, что это за библиотека) - person freakish; 18.04.2019