Javascript-форум (https://javascript.ru/forum/)
-   Элементы интерфейса (https://javascript.ru/forum/dom-window/)
-   -   Как реализовать задачу с одновременными запросами на сервер? (https://javascript.ru/forum/dom-window/83036-kak-realizovat-zadachu-s-odnovremennymi-zaprosami-na-server.html)

dc65k 31.08.2021 13:25

Как реализовать задачу с одновременными запросами на сервер?
 
Всем привет. Есть функция, она принимает на вход массив адресов на которые будет происходить запрос и количество максимальных запросов (количество максимальных запросов в очереди). Если количество запросов больше чем второй параметр (очередь заполнена), то мы должны дождаться когда можно будет уменьшить очередь и продолжить делать следующие запросы по описанному принципу заполнения очереди.
Ниже я написал начало решения, вопрос именно в том как в коде написать очистку очереди и продолжение выполнение запросов?
const sendRequests = (requests, maxRequestsCount) => {
   
    return new Promise(resolve => {

        const responses = [];

        let requestsCount = 0;

        for (let i = 0; i < requests.length; i++) {
            const request = requests[i];

            requestsCount += 1;

            if (requestsCount !== maxRequestsCount) {
                makeRequest(request);
            }
        }

        function makeRequest(url) {
            return fetch(url)
                .then(result => result.json())
                .catch(error => error)
                .then(result => {
                    responses.push(result);
                    resolve(responses);
                });
        }
    });
}

console.log(
    sendRequests([
        'https://jsonplaceholder.typicode.com/todos/2',
        'https://jsonplaceholder.typicode.com/todos/4',
        'https://jsonplaceholder.typicode.com/todos/10',
        'https://jsonplaceholder.typicode.com/todos/18'
    ], 3).then(response => {
        console.log(response);
    })
);

ksa 31.08.2021 15:37

Цитата:

Сообщение от dc65k
то мы должны дождаться когда можно будет уменьшить очередь

И когда это наступает?

dc65k 31.08.2021 15:47

Очередь считается заполненной, когда ушло максимальное количество запросов (второй параметр). Очистить очередь на один запрос можно тогда, когда этот запрос выполнился.

voraa 31.08.2021 18:24

Может такой вариант
const sendRequests = (requests, maxRequestsCount) => {
    
    return new Promise(resolve => {
 
        const responses = Array.from({length: requests.length}, _ => null);
 
		let curUrlInd = 0;
		let requestDone = 0;
 
        for (let i = 0; i < maxRequestsCount; i++) {
            makeRequest(requests[i], i);
        }

        curUrlInd = maxRequestsCount; 

        function makeRequest(url, n) {
            return fetch(url)
                .then(result => result.json())
                .catch(error => error)
                .then(result => {
                    responses[n] = result;
                })
                .finally (() => {
					if (curUrlInd < requests.length) {
						makeRequest(requests[curUrlInd], curUrlInd);
						curUrlInd++;
					}
					requestDone ++;
					if (requestDone === requests.length) {
						resolve(responses);
					}
                });
        }
    });
}
 
console.log(
    sendRequests([
        'https://jsonplaceholder.typicode.com/todos/2',
        'https://jsonplaceholder.typicode.com/todos/4',
        'https://jsonplaceholder.typicode.com/todos/10',
        'https://jsonplaceholder.typicode.com/todos/18'
    ], 3).then(response => {
        console.log(response);
    })
);

ksa 31.08.2021 19:10

Цитата:

Сообщение от dc65k
Очистить очередь на один запрос можно тогда, когда этот запрос выполнился.

Тогда и запускай следующий УРЛ... И так пока они не кончатся.

Aetae 31.08.2021 20:20

Цитата:

Сообщение от voraa (Сообщение 539892)
Может такой вариант
const responses = Array.from({length: requests.length}, _ => null);

Зачема так усложнять? Есть же классика:
const responses = new Array(requests.length);


P.S. И да, работать как требует dc65k оно не будет: осле первых трёх количество запросов будет нарастать без контроля, а должно быть всегда три.)

voraa 31.08.2021 20:36

Цитата:

Сообщение от Aetae
Зачема так усложнять? Есть же классика:
const responses = new Array(requests.length);

Классика - не значит хорошо.
В данном случае, конечно без разницы, но я так не делаю
new Array(n) создает "дырявый" массив, который очень неэффективен при всяких оптимизациях.

voraa 31.08.2021 20:41

Цитата:

Сообщение от Aetae
P.S. И да, работать как требует dc65k оно не будет: осле первых трёх количество запросов будет нарастать без контроля, а должно быть всегда три.)

Заменим fetch на другую функцию и проверим
<html>
<script>
const fakefetch = (url) =>{
	return new Promise (res => {
		dt = (Math.random()*2+1)*1000 | 0
		setTimeout(() => res({url:url, dt: dt}), dt)
	})
}
const sendRequests = (requests, maxRequestsCount) => {
    
    return new Promise(resolve => {
 
        const responses = Array.from({length: requests.length}, _ => null);
 
		let curUrlInd = 0;
		let requestDone = 0;
 
        
        for (let i = 0; i < maxRequestsCount; i++) {
            const request = requests[i];
			 
            makeRequest(requests[i], i);
        }
        curUrlInd = maxRequestsCount
 
        async function makeRequest(url, n) {
			console.log('Request', n)
            return fakefetch(url)
//                .then(result => result.json())
//                .catch(error => error)
                .then(result => {
                    responses[n] = result;
                })
                .finally (() => {
					requestDone ++;
					console.log('Done', n, requestDone)
					if (curUrlInd < requests.length) {
						makeRequest(requests[curUrlInd], curUrlInd);
						curUrlInd++;
					}
					if (requestDone === requests.length) {
						console.log ('Done All')
						resolve(responses);
					}
                });
        }
    });
}
 
    sendRequests([
        'https://jsonplaceholder.typicode.com/todos/2',
        'https://jsonplaceholder.typicode.com/todos/4',
        'https://jsonplaceholder.typicode.com/todos/10',
        'https://jsonplaceholder.typicode.com/todos/18',
        'https://jsonplaceholder.typicode.com/todos/20',
        'https://jsonplaceholder.typicode.com/todos/40',
        'https://jsonplaceholder.typicode.com/todos/100',
        'https://jsonplaceholder.typicode.com/todos/180'
    ], 3).then(response => {
        console.log(response);
    })
</script>
</html>


Нарастать не будет - новый запрос посылается, когда какой то из ранее посланных закончится.

Aetae 31.08.2021 20:55

voraa, да согласен, тупанул.)

По поводу new Array - я его использую только именно в таком случае и ни в каком ином. Считаю что использовать подходящие инструменты в подходящих местах - стоит, а откидывать те или иные вещи скопом только потому, что в некоторых ситуациях(прекрасно известных) они могут привести к неприятным последствиями, ограничивая себя узкими рамками "безопасного" кода - не стоит.)

Обычно всё это сводится к "а вот к команде присоединится говнокодер Вася, и всё сломает", но я не хочу и не буду ради какого-то Васи писать менее органичный и красивый код.)

Vlasenko Fedor 31.08.2021 21:45

dc65k,
Ответь пожалуйста, для чего нужно подобное извращение?

Vlasenko Fedor 31.08.2021 22:38

Цитата:

Сообщение от voraa
new Array(n) создает "дырявый" массив, который очень неэффективен при всяких оптимизациях.

Array(n).fill(0)

Aetae 31.08.2021 22:41

Vlasenko Fedor, ну например возможно jsonplaceholder держит только три соединения, а больше - дропает.)
Частая ситуация, так-то.

Vlasenko Fedor 31.08.2021 22:52

Aetae,
сервер для одного клиента ?
для чего тогда балансировщики
и что делать с ошибками в таком случае?

Aetae 31.08.2021 23:23

Vlasenko Fedor, ты странный. Обычная антиспам\ антидос\ антибрутфорс система. На эндпоинт один клиент может послать только N запросов за раз. Это буквально везде.
Балансировщики - балансируют разные запросы разных клиентов меж разными серверами, они тут не причём.
С ошибками делать то же что и всегда - как-либо обрабатывать.

Vlasenko Fedor 01.09.2021 00:07

Цитата:

Сообщение от Aetae
Обычная антиспам\ антидос\ антибрутфорс система.

Пишешь, о том в чем не разбираешься
Ибо в эти системах есть понятие время в количество запросов, а не колличество только
Потому я изадал вопрос ТС, чтоб он пояcнил зачем это
Пример http://nginx.org/ru/docs/http/ngx_ht...eq_module.html

Aetae 01.09.2021 00:47

Vlasenko Fedor, што?
Вот прямо по твоей ссылке: ставим
limit_req zone=one burst=3 nodelay;
получаем именно такой результат.

"N запросов за раз" - очевидно очевидно значит "N запросов за минимальный промежуток времени установленный на сервере в подавляющем большинстве случаев равный одной секунде". Ты думал я о планковских величинах тут говорю, или что?

Vlasenko Fedor 01.09.2021 01:02

Aetae,
неужели непонятно, что лимит запросов идет по времени
давй на пальцах, как ты поймешь что превышен лимит ?
будешь считать запросы и говорить, что 4 уже с 503 ответом?
нет ты будешь смотреть в каком интервале времени прошли запросы
вот ответ на твой пример

Aetae 01.09.2021 01:32

Vlasenko Fedor,
Цитата:

будешь считать запросы и говорить, что 4 уже с 503 ответом?
нет ты будешь смотреть в каком интервале времени прошли запросы
Эти фразы друг другу не противоречат: "будешь считать запросы на интервале времени rate(который в 99% равен 1r/s) и говорить, что 4 уже с 503 ответом". Одна секунда и есть условно одновременно.

Или ты намекаешь, что может быть такой случай, когда ответы на все три запроса придут раньше чем через секунду, из-за чего четвёртый запрос будет отправлен до истечения секундного интервала из-за чего получится 4 запроса в секунду и он будет дропнут? Конкретно в данном случае сам механизм работы burst не допустит этого, но и в целом в реальном мире это крайне маловероятно.
В идеальном случае действительно желательно точно знать, что за механизмы и тайминги используются там на сервере, и высчитывать доп задержку для надёжности, если ты имел ввиду именно это.

voraa 01.09.2021 09:41

Цитата:

Сообщение от Vlasenko Fedor
Сообщение от voraa
new Array(n) создает "дырявый" массив, который очень неэффективен при всяких оптимизациях.
Array(n).fill(0)

Я же не про то, что все элементы заполнены, а с точки зрения оптимизации. Я ориентируюсь в этом на V8, как самый распространенный (это и Chrome, и Edge, и Node).

https://v8.dev/blog/elements-kinds
Конкретнее
https://v8.dev/blog/elements-kinds#avoid-creating-holes

Если массив стал (изначально создан) дырявым, то он дырявым и останется, что с ним не делай. V8 будет считать его дырявым, и не применять к нему оптимизаций, как к заполненному.
Я когда то делал тесты. Выигрыш может и не самый впечатляющий - 5-7%. Но все таки выигрыш.

Aetae 01.09.2021 12:25

voraa, ну это прям наносекунды. Тогда и методы массива вообще юзать не надо - просто циклы. Конечно же никакого spread и destructuring. А ещё можно выиграть на создании объектов вот так, да: https://v8.dev/blog/cost-of-javascript-2019#json .)

voraa 01.09.2021 12:39

Конечно можно считать, что в гуглах сидят программеры, которые от избытка денег и времени делают никому не нужные оптимизации.
Цитата:

Сообщение от Aetae
Конечно же никакого spread и destructuring.

Вроде утверждается, что это неплохо оптимизированно, касаемо массивов.

Aetae 01.09.2021 12:48

voraa, неплохо. Но всё равно в 100500 раз больше времени занимает, чем потенциальные расходы от дырок.)
По твоей же ссылке прямо пишут "the performance difference between accessing holey or packed arrays is usually too small to matter or even be measurable".

Alexandroppolus 01.09.2021 13:57

Цитата:

Сообщение от dc65k
const sendRequests = (requests, maxRequestsCount) => {

У такого подхода есть баг: если мы отправим 2 порции запросов, то они пойдут параллельно, т.е. будет в два раза больше запросов одновременно - у них ведь 2 независимые очереди.
Запилил исправленную версию, которая позволяет отправлять несколько порций. Причем, вторая порция может начать исполняться ещё до того, как завершится первая.
function createSendRequests(func, limit) {
    let reqCount = 0;
    const queue = [];
    return function (requests) {
        console.log('sendRequests for ', requests.join(','));
        if (!requests || !requests.length) {
            return Promise.resolve([]);
        }
        return new Promise((resolve) => {
            const responses = Array.from({length: requests.length});
            let curUrlInd = 0;
            let requestDone = 0;

            while (curUrlInd < requests.length && reqCount < limit) {
                makeRequest(curUrlInd);
            }

            let inQueue = curUrlInd < requests.length;
            if (inQueue) {
                queue.push(nextRequest);
            }

            function nextRequest() {
                makeRequest(curUrlInd);
                if (inQueue && curUrlInd === requests.length) {
                    inQueue = false;
                    queue.shift();
                }
            }

            function makeRequest(n) {
                reqCount++;
                curUrlInd++;
                const req = requests[n];
                console.log('request for ', req);
                func(req).then(data => {
                    console.log('response for ', req);
                    responses[n] = {
                        data
                    };
                }, (err) => {
                    responses[n] = {
                        error: err || 'error'
                    };
                }).finally (() => {
                    reqCount--;
                    if (queue.length) {
                        queue[0]();
                    }
                    requestDone++;
                    if (requestDone === requests.length) {
                        resolve(responses);
                    }
                });
            }
        });
    };
}

// ----- использование -------------------------------------

function myFetch(req) {
    return new Promise((res) => {
        setTimeout(res, 1000, 'response for ' + req);
    });
}

var sendRequests = createSendRequests(myFetch, 5);

sendRequests([
    'reqA',
    'reqB',
]).then((data) => {
    console.log('responses 1', data);
});

sendRequests([
    'req1',
    'req2',
    'req3',
    'req4',
    'req5',
    'req6',
    'req7',
]).then((data) => {
    console.log('responses 2', data);
});

sendRequests([
    'req8',
    'req9',
    'req10',
    'req11',
    'req12',
]).then((data) => {
    console.log('responses 3', data);
});


Часовой пояс GMT +3, время: 06:05.