Алоха!
Самым, наверное, интересным событием на этой неделе в мире Java стала конференция JPoint, которая прошла в Центре Международной Торговли в Москве. Одноклассники предложили посетителям тоже поучаствовать в разработке самой высоконагруженной системы на Java и помочь нашим разработчикам в решении практических задач, с которыми они сталкиваются в своей работе.
Одиннадцать человек, лучше всех решивших эти задачки, уже получили от нас призы, ну а для остальных мы запилили этот пост с разбором решений.
Чтобы вам удобнее было сначала попробовать решить задачки самим, мы скрыли правильные ответы под спойлером. Чур, открывать только после того, как сами додумались до решения ;-)
Поехали!
Вадим написал следующий код для партиционирования музыкальных треков по серверам. Что он сделал не так? Исправьте ошибку Вадима в коде:
Леонид написал такой код для очистки временных файлов из директории. Что он сделал не так?
Даша мигрирует код с Java 10 на legacy систему, работающую под Java 8. На что ей нужно заменить
Выберите подходящие ответы:
Антон ограничивает количество запросов к детектору лиц на пользовательских фото следующим кодом. Как ему реализовать изменение лимита на лету потокобезопасным способом?
Вот и все! Всем спасибо, что приняли участие в решении наших задачек. Отдельное спасибо тем, кто написал нам свое мнение по поводу наших задачек, приходил к нам на стенд и спорил с нами о решениях!
А может вы знаете лучшие решения? Добро пожаловать в комменты!
Самым, наверное, интересным событием на этой неделе в мире Java стала конференция JPoint, которая прошла в Центре Международной Торговли в Москве. Одноклассники предложили посетителям тоже поучаствовать в разработке самой высоконагруженной системы на Java и помочь нашим разработчикам в решении практических задач, с которыми они сталкиваются в своей работе.
Одиннадцать человек, лучше всех решивших эти задачки, уже получили от нас призы, ну а для остальных мы запилили этот пост с разбором решений.
Чтобы вам удобнее было сначала попробовать решить задачки самим, мы скрыли правильные ответы под спойлером. Чур, открывать только после того, как сами додумались до решения ;-)
Поехали!
Задача о Вадиме и партитуре
Вадим написал следующий код для партиционирования музыкальных треков по серверам. Что он сделал не так? Исправьте ошибку Вадима в коде:
/**
* @return partition between 0 inclusive
* and partitions exclusive
*/
int partition(Track track, int partitions) {
assert track != null;
assert partitions > 0;
return track.hashCode() % partitions;
}
Решение
Очевидная проблема в коде Вадима в том, что track.hashCode() может возвращать как положительные, так и отрицательные значения.
Вадим же человек аккуратный (вы посмотрите, сколько assertов!), значит, комментарий к методу он тоже написал аккуратно, а там указано, что метод вернет число в интервале от 0 до partitions исключительно.
Очевидным способом исправить проблему будет немного другая концовка метода, например, такая:
Казалось бы — ура и в продакшен! Но есть одна менее очевидная проблема — по спецификации hashCode() вполне себе может вернуть и Integer.MIN_VALUE, а попытка взять от такого числа Math.abs ни к чему хорошему не приведет.
Ох, говорила мама, следи за скобками, сынок! Правильно будет как-то так:
Другая, более серьезная проблема Вадима гораздо менее очевидна. Способ, который он выбрал, чтобы распределять данные по серверам, неконсистентен: при изменении количества партиций (например, при добавлении серверов) все треки перераспределяются по кластеру практически полностью. Если треков мало — проблем немного, а если кластер большой и треков там на сотни терабайт — их все надо переместить между серверами. А Вадим уже залил терабайты треков на сотню — другую серверов…
Эх, Вадим, Вадим! Использовал бы ты какой-либо из вариантов консистентных хешей, не болела бы у тебя сейчас голова.
Вадим же человек аккуратный (вы посмотрите, сколько assertов!), значит, комментарий к методу он тоже написал аккуратно, а там указано, что метод вернет число в интервале от 0 до partitions исключительно.
Очевидным способом исправить проблему будет немного другая концовка метода, например, такая:
return Math.abs( track.hashCode() ) % partitions;
Казалось бы — ура и в продакшен! Но есть одна менее очевидная проблема — по спецификации hashCode() вполне себе может вернуть и Integer.MIN_VALUE, а попытка взять от такого числа Math.abs ни к чему хорошему не приведет.
Ох, говорила мама, следи за скобками, сынок! Правильно будет как-то так:
return Math.abs( track.hashCode() % partitions );
Другая, более серьезная проблема Вадима гораздо менее очевидна. Способ, который он выбрал, чтобы распределять данные по серверам, неконсистентен: при изменении количества партиций (например, при добавлении серверов) все треки перераспределяются по кластеру практически полностью. Если треков мало — проблем немного, а если кластер большой и треков там на сотни терабайт — их все надо переместить между серверами. А Вадим уже залил терабайты треков на сотню — другую серверов…
Эх, Вадим, Вадим! Использовал бы ты какой-либо из вариантов консистентных хешей, не болела бы у тебя сейчас голова.
Леонид работает дворником
Леонид написал такой код для очистки временных файлов из директории. Что он сделал не так?
void cleanup(Path dir) throws IOException {
for (Path file : Files.newDirectoryStream(dir)) {
if (file.endsWith(".tmp")) {
Files.delete(file);
}
}
}
Решение
Леонид очень торопился выкатить очистку мусора в продакшен и невнимательно изучил документацию к DirectoryStream. DirectoryStream implements AutoCloseable. То есть имеет метод close(). А если что-то имеет такой метод — он должен вызываться. Последствия не заставили себя ждать — выкатив новую версию в продакшен, он обнаружил утечку нативной памяти в java процессе.
Поскольку релиз включал не только этот код, источник утечки однозначно определить было нельзя, и Леониду пришлось изучить, как можно определять нативные утечки. При профилировании нашлась утечка при вызове нативного метода __alloc_dir, который как раз вызывается в результате вызова Files.newDirectoryStream. Леонид хлопнул себя по лбу и исправил проблему:
— «Ура!», сказал Леонид и выкатил релиз. И снова поторопился.
Ведь еще неплохо было бы проверить, что file из стрима является файлом, а не директорией, например, добавив условие с Files.isRegularFile(file).
Также Леонид забыл, что Files.delete может выбросить IOException, и процесс очистки прервётся — с этим тоже надо что-то делать.
«Зря я пошел в дворники», подумал про себя Леонид…
Поскольку релиз включал не только этот код, источник утечки однозначно определить было нельзя, и Леониду пришлось изучить, как можно определять нативные утечки. При профилировании нашлась утечка при вызове нативного метода __alloc_dir, который как раз вызывается в результате вызова Files.newDirectoryStream. Леонид хлопнул себя по лбу и исправил проблему:
void cleanup(Path dir) throws IOException {
try ( DirectoryStream<Path> stream = Files.newDirectoryStream(dir) ) {
for (Path file : stream) {
if (file.endsWith(".tmp")) {
Files.delete(file);
}
}
}
}
— «Ура!», сказал Леонид и выкатил релиз. И снова поторопился.
Ведь еще неплохо было бы проверить, что file из стрима является файлом, а не директорией, например, добавив условие с Files.isRegularFile(file).
Также Леонид забыл, что Files.delete может выбросить IOException, и процесс очистки прервётся — с этим тоже надо что-то делать.
«Зря я пошел в дворники», подумал про себя Леонид…
Бедная Даша
Даша мигрирует код с Java 10 на legacy систему, работающую под Java 8. На что ей нужно заменить
var
, чтобы программа скомпилировалась? var list = Arrays.asList("1", 2, 3.0);
Выберите подходящие ответы:
List<?>
List<? super Comparable>
List<? extends Comparable>
List<? extends Comparable & Serializable>
List<? super Comparable & Serializable>
Решение
«Боже, на что я трачу свою жизнь», подумала Даша и быстро нашла правильные варианты: конечно же, первый, второй и третий.
Антон, щи и лимиты
Антон ограничивает количество запросов к детектору лиц на пользовательских фото следующим кодом. Как ему реализовать изменение лимита на лету потокобезопасным способом?
class ConcurrencyLimiter {
static final int DEFAULT_LIMIT = 10;
final Semaphore semaphore = new Semaphore(DEFAULT_LIMIT);
void runTask(Runnable task) {
semaphore.acquireUninterruptibly();
try {
task.run();
} finally {
semaphore.release();
}
}
// Implement me
void setLimit(int newLimit) {
assert newLimit > 0;
// TODO: Implementation pending
}
}
Решение
«Многопоточность — это сложно» — в очередной раз прочитал Антон мудрость древних с плаката и сочинил вот такой код:
Естественно, что он не единственный верный, тут возможны различные вариации, например, без CAS, но с синхронизацией всего setLimit, это непринципиально.
Но Антон не учел, что семафор-то он объявил unfair в самом начале, и попытка получить сразу много пермитов выражением semaphore.acquireUninterruptibly(previous — newLimit) может никогда не завершиться, особенно если поступает очень много задач на определение лиц, и в семафоре никогда не образуется достаточное количество пермитов за 1 раз.
Данную проблему Антон может решить с помощью вот такого цикла, который берет пермиты по одному:
«А может, это не лучшее решение?» — задумался Антон, ведь «Многопоточность — это сложно» — в очередной раз прочитал Антон мудрость древних с плаката висящего над его столом…
// Implemented
final AtomicInteger limit = new AtomicInteger(DEFAULT_LIMIT);
void setLimit(int newLimit) {
assert newLimit > 0;
final int previous = limit.getAndSet(newLimit);
if (newLimit >= previous) {
semaphore.release(newLimit - previous);
} else {
semaphore.acquireUninterruptibly(previous - newLimit);
}
}
Естественно, что он не единственный верный, тут возможны различные вариации, например, без CAS, но с синхронизацией всего setLimit, это непринципиально.
Но Антон не учел, что семафор-то он объявил unfair в самом начале, и попытка получить сразу много пермитов выражением semaphore.acquireUninterruptibly(previous — newLimit) может никогда не завершиться, особенно если поступает очень много задач на определение лиц, и в семафоре никогда не образуется достаточное количество пермитов за 1 раз.
Данную проблему Антон может решить с помощью вот такого цикла, который берет пермиты по одному:
// Implemented
final AtomicInteger limit = new AtomicInteger(DEFAULT_LIMIT);
void setLimit(int newLimit) {
assert newLimit > 0;
final int previous = limit.getAndSet(newLimit);
if (newLimit >= previous) {
semaphore.release(newLimit - previous);
} else {
// magic goes here:
for ( int i = 0; i < previous - newLimit; i++)
semaphore.acquireUninterruptibly(1);
}
}
«А может, это не лучшее решение?» — задумался Антон, ведь «Многопоточность — это сложно» — в очередной раз прочитал Антон мудрость древних с плаката висящего над его столом…
Вот и все! Всем спасибо, что приняли участие в решении наших задачек. Отдельное спасибо тем, кто написал нам свое мнение по поводу наших задачек, приходил к нам на стенд и спорил с нами о решениях!
А может вы знаете лучшие решения? Добро пожаловать в комменты!
Комментарии (3)
gnkoshelev
08.04.2018 02:38А может вы знаете лучшие решения? Добро пожаловать в комменты!
Вызов принят! Лучшие-нелучшие, но есть, что обсуждать. :)
nkapustin
10.04.2018 15:34+1Интересный момент что код Леонида с очисткой директории делает не то что ожидается, он хочет удалить все файлы с расширением ".tmp", но нет, будет удален только файл с названием ".tmp", ведь был использован метод Path::endsWith, который делает совсем не тоже самое что метод String::endsWith.
saboteur_kiev
Чисто любопытно — никто не создает директории с расширением .tmp?
Ведь если кто-то создал такую директорию, то надо ее тоже «чистить», а не игнорировать.