
Desafio Parallel Download
Há algumas semanas, fiz uma entrevista e um dos desafios era fazer o download de alguns arquivos em paralelo. Achei muito interessante, então resolvi fazer um artigo sobre isso. Falando um pouco do desafio, me foi fornecida a função pooledDownload, que recebe uma lista de URLs, uma função connect que retorna uma conexão, o número máximo de conexões permitidas e uma lista de URLs que devem ser baixadas e salvas; a ação deve ser feita em paralelo.
Critérios de aceitação
- Todos os arquivos devem ser baixados e salvos.
- O conteúdo do arquivo deve ser salvo assim que o arquivo for baixado.
- Os downloads devem ser distribuídos igualmente entre as conexões.
- Cada conexão só pode baixar um arquivo por vez. Para baixar mais arquivos em paralelo, crie mais conexões.
- Qualquer conexão aberta deve ser fechada.
- Se não for possível abrir uma conexão com o servidor, você deve rejeitar com um
Errorcontendo a mensagemConnection Failed. - Se um erro ocorrer durante o download, você deve rejeitar com o mesmo erro.
- Às vezes, o servidor pode não ter slots para lidar com o número de conexões simultâneas. Nesse caso, você deve parar a abertura de novas conexões quando o servidor alcançar o limite.
const pooledDownload = async (connect, save, downloadList, maxConcurrency) => { // Implemente a função aqui};module.exports = pooledDownload;Solução
O primeiro passo que eu fiz foi criar a função getConnections, ela recebe a função connect e o número
máximo de conexões permitidas, e retorna um array de conexões abertas.
const getConnections = async (connect, maxConcurrency) => { const connections = []; for (let i = 0; i < maxConcurrency; i++) { try { connections.push(await connect()); } catch (e) { break; } } return connections;};Eu sei que o break no catch, não é uma boa prática, mas é uma maneira simples de assegurar que estejamos em conformidade com o critério 8.
Agora que temos as conexões abertas, podemos implementar uma parte da função pooledDownload.
const pooledDownload = async (connect, save, downloadList, maxConcurrency) => { const filesToDownload = downloadList.slice(0); const maxConnectionNeeded = Math.min(maxConcurrency, downloadList.length); const connections = await getConnections(connect, maxConnectionNeeded); if (!connections || !connections.length) { throw new Error("connection failed"); }};A primeira ação que fiz foi criar uma cópia da lista de arquivos a serem baixados, e depois pegar o mínimo entre o
número de conexões permitidas e o número de arquivos a serem baixados, e então abrir as conexões. Se não for possível
abrir nenhuma conexão, eu rejeito a Promise com um Error contendo a mensagem Connection Failed.
Você pode se perguntar o porquê de usar o mínimo entre o número de conexões permitidas e o número de arquivos a serem baixados, isso é para garantir que não abriremos mais conexões do que o necessário.
O próximo passo é criar um loop que irá baixar os arquivos.
const promise = [];for (let i = 0; i < maxConnectionNeeded; i++) { promise.push(execute(connections, filesToDownload, save));}try { await Promise.all(promise);} finally { // fechar conexões}A função execute é responsável por baixar os arquivos, e é onde a magia acontece.
1const execute = (connections, downloadList, save) => {2 if (downloadList.length === 0 || connections.length === 0) return Promise.resolve();34 const currentDownload = downloadList.shift();5 const connection = connections.pop();67 return connection8 .download(currentDownload)9 .then((result) => {10 save(result);11 connections.unshift(connection);12 return execute(connections, downloadList, save);13 })14 .catch((e) => {15 downloadList.unshift(currentDownload);16 return Promise.reject(e);17 });18};O que acontece na função execute é que eu verifico se ainda tem arquivos para serem baixados e se ainda tem
conexões abertas, se não tiver eu retorno uma promessa resolvida. Então eu pego o primeiro arquivo da lista de arquivos
a serem baixados, e a primeira conexão aberta, e então eu baixo o arquivo, salvo o conteúdo do arquivo, e então eu
coloco a conexão de volta no início do array de conexões, e chamo a função execute novamente. Se um erro ocorrer
durante o download, eu coloco o arquivo de volta na lista de arquivos a serem baixados, e rejeito a promessa com o mesmo
erro.
Por fim, eu fecho as conexões.
try { await Promise.all(promise);} finally { connections.forEach((connection) => connection.close());}Conclusão
Esse foi um desafio muito interessante, gostei muito de realizá-lo, aprendi bastante e espero que você também tenha aprendido. Se você tiver alguma dúvida, sugestão ou crítica, por favor, mande uma mensagem. Vou adorar ouvir o que você tem a dizer.
O resultado final fica assim:
1const getConnections = async (connect, maxConcurrency) => {2 const connections = [];3 for (let i = 0; i < maxConcurrency; i++) {4 try {5 connections.push(await connect());6 } catch (e) {7 break;8 }9 }1011 return connections;12};1314const execute = (connections, downloadList, save) => {15 if (downloadList.length === 0 || connections.length === 0) return Promise.resolve();1617 const currentDownload = downloadList.shift();18 const connection = connections.pop();1920 return connection21 .download(currentDownload)22 .then((result) => {23 save(result);24 connections.unshift(connection);25 return execute(connections, downloadList, save);26 })27 .catch((e) => {28 downloadList.unshift(currentDownload);29 return Promise.reject(e);30 });31};3233const pooledDownload = async (connect, save, downloadList, maxConcurrency) => {34 const filesToDownload = downloadList.slice(0);35 const maxConnectionNeeded = Math.min(maxConcurrency, downloadList.length);36 const connections = await getConnections(connect, maxConnectionNeeded);3738 if (!connections || !connections.length) {39 throw new Error("connection failed");40 }4142 const promise = [];43 for (let i = 0; i < maxConnectionNeeded; i++) {44 promise.push(execute(connections, filesToDownload, save));45 }4647 try {48 await Promise.all(promise);49 } finally {50 connections.forEach((connection) => connection.close());51 }52};5354module.exports = pooledDownload;