Неконтролируемый рост системных файлов Linux

Интересную особенность работы ASP.NET Core приложения в Linux мне удалось недавно зафиксировать. Было установлено, что со временем системынй файлы Linux (а именно: syslog и daemon.log) вырастают до угрожающих размеров. Оказалось, что весь консольный вывод ASP.NET Core приложений попадал туда. А консольный вывод включал помимо AddConsole() ещё и консоль Serilog Write().ConsoleColored(). Выставление корректных настроек секции Logging конфигурационных файлов appsetting.json позволил установить их уровень высоким и существенно уменьшить размеры системных файлов Linux.

1
2
3
4
5
6
7
"Logging": {
"Console": {
"LogLevel": {
"*": "Error"
}
}
}

Оказывается, что системная служба journald ведёт логирование вывода на консоль в свои бинарные логи, а кроме того вносит записи о выводе на консоль в системные журналы Linux (syslog и daemon.log). Поэтому будьте внимательны при выставлении уровня логирования ваших приложений, особенно на продакшене.

Сопоставление с образцом на основе кортежей

Ранее мною уже был рассмотрен один из механизмов сопоставления с образцом (паттерн матчинга) в статье Сопоставление с образцом на основе значений свойств объекта. В данной статье рассмотрим сопоставление с образцом на основе кортежей, что делает код намного короче.

Рассмотрим класс комнаты и перечисление статуса члена клуба такого вида:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Room
{
public int Number { get; set; }
public string Type { get; set; }
public string Size { get; set; }

public void Deconstruct(out string type, out string size)
{
type = type;
size = Size;
}
}

public enum Membership
{
Bronze,
Silver,
Gold
}

Метод GetRooms возвращает список комнат вида:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static List<Room> GetRooms()
{
return new List<Room>
{
new Room
{
Number = 3,
Size = "Large",
Type = "Deluxe"
},
new Room
{
Number = 2,
Size = "Large",
Type = "Regular"
},
new Room
{
Number = 1,
Size = "Standard",
Type = "Regular"
}
};
}

Начиная с C# 8.0 появилась возможность сопоставлять с образцом в выражении switch, используя деконструктор класса (см. ранее конструкцию Deconstruct в определении класса Room):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const int RoomNotAvailable = -1;

public static int AssignRoom(Membership membership)
{
foreach(var room in GetRooms())
{
Membership clientCategory = room switch
{
("Deluxe", "Large") => Membership.Gold,
("Regular", "Large") => Membership.Silver,
("Regular", "Standard") => Membership.Bronze,
_ => Membership.Bronze
};

if (clientCategory == membership)
return room.Number;
}

return RoomNotAvailable;
}

Код ветки _ => Membership.Bronze аналогичен случаю default: в стандартном switch.

Тогда основной метод программы может использовать вышеприведённый код таким образом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static voir Main()
{
Console.Write("Choose status: 0 for Bronze, 1 for Silver, 2 for Gold");
string clientStatus = Console.ReadLine();

Enum.TryParse(clientStatus, out Membership membership);

int roomNumber = AssignRoom(membership);

if (roomNumber == RoomNotAvailable)
Console.WriteLine("Room not available");
else
Console.WriteLine($"Room number is {roomNumber}");
}

Видно на сколько короче стала конструкция switch благодаря использованию кортежей. Компактный синтаксис кортежей делает их идеальным инструментом для сопоставления с образцом (паттерн матчинга). В остальном код и результаты идентичны приведённому в статье Сопоставление с образцом на основе значений свойств объекта.

ToDictionary vs ToLookup

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

1
2
3
4
5
6
7
8
9
string[] names = {"Pavel", "Peter", "Andrew", "Anna", "Alice", "John"};
var namesByLetter = new Dictionary<char, List<string>>();

foreach (var group in names.GroupBy(name => name[0]))
namesByLetter.Add(group.Key, group.ToList());

Assert.That(namesByLetter['J'], Is.EquivalentTo(new[] { "John" }));
Assert.That(namesByLetter['P'], Is.EquivalentTo(new[] {"Pavel", "Peter"}));
Assert.IsFalse(namesByLetter.ContainsKey('Z'));

Ровно того же эффекта можно добиться и без цикла при помощи LINQ-метода ToDictionary, имеющего следующую сигнатуру:

1
IDictionary<K, V> ToDictionary(this IEnumerable<T> items, Func<T, K> keySelector, Func<T, V> valueSelector)

Тогда предыдущий пример с foreach в случае ToDictionary примет следующий вид:

1
2
3
4
5
6
7
8
9
string[] names = {"Pavel", "Peter", "Andrew", "Anna", "Alice", "John"};

Dictionary<char, List<string>> namesByLetter = names
.GroupBy(name => name[0])
.ToDictionary(group => group.Key, group => group.ToList());

Assert.That(namesByLetter['J'], Is.EquivalentTo(new[] { "John" }));
Assert.That(namesByLetter['P'], Is.EquivalentTo(new[] {"Pavel", "Peter"}));
Assert.IsFalse(namesByLetter.ContainsKey('Z'));

Ещё проще воспользоваться LINQ-методом ToLookup, имеющим следующие сигнатуры:

1
ILookup<K, T> ToLookup(this IEnumerable<T> items, Func<T, K> keySelector)
1
ILookup<K, V> ToLookup(this IEnumerable<T> items, Func<T, K> keySelector, Func<T, V> valueSelector)

В отличие от словаря, Lookup является неизменяемым (immutable) типом. У него нет методов типа Add и открытого конструктора. Интересно, что Lookup по неизвестному ключу возвращает пустую коллекцию, а Dictionary в такой ситуации выбрасывает исключение. Также в Lookup можно использовать ключи типа null.
Замечу, что операции list.ToLookup(x => x) и list.GroupBy(x => x).ToDictionary(group => group.Key) семантически эквивалентны.

Пример с foreach и ToDictionary в случае ToLookup примет следующий вид:

1
2
3
4
5
6
7
8
9
10
string[] names = {"Pavel", "Peter", "Andrew", "Anna", "Alice", "John"};

ILookup<char, string> namesByLetter = names.ToLookup(name => name[0], name => name.ToLower());

Assert.That(namesByLetter['J'], Is.EquivalentTo(new[] {"john"}));
Assert.That(namesByLetter['P'], Is.EquivalentTo(new[] {"pavel", "peter"}));

// Lookup по неизвестному ключу возвращает пустую коллекцию.
// Это бывает удобнее, чем поведение Dictionary, который в такой ситуации бросает исключение.
Assert.That(namesByLetter['Z'], Is.Empty);

В русскоязычной литературе Lookup раньше именовался таблицей истинности. Lookup можно использовать в построении обратного индекса. Обратный индекс — это структура данных, часто использующаяся в задачах полнотекстового поиска нужного документа в большой базе документов. По своей сути обратный индекс напоминает индекс в конце бумажных энциклопедий, где для каждого ключевого слова указан список страниц, где оно встречается.

Допустим наш документ определён так:

1
2
3
4
5
public class Document
{
public int Id;
public string Text;
}

Тогда для списка документов обратный индекс можно получить так:

1
2
3
4
5
6
7
8
9
public static ILookup<string, int> BuildInvertedIndex(Document[] documents)
{
return documents
.SelectMany(x => Regex.Split(x.Text, @"\W+")
.Where(x => x.Length > 0)
.Select(y => Tuple.Create(y.ToLower(), x.Id)))
.Distinct()
.ToLookup(x => x.Item1, x => x.Item2);
}

Сортировка кортежей

Создание кортежа с помощью конструктора выглядит громоздко. Чтобы облегчить синтаксис создания кортежей существует класс Tuple с серией статических методов, создающих кортежи:

1
2
3
var t1 = Tuple.Create(42, "abc");
// Эквивалентно:
// var t1 = new Tuple<int, string>(42, "abc");

Полезное свойство кортежей состоит в том, что они реализуют интерфейс IComparable, сравнивающий кортежи по компонентам. То есть Tuple.Create(1, 2) будет меньше Tuple.Create(2, 1). Этот интерфейс по умолчанию используется в методах сортировки и поиска минимума/максимума.

Использование данного факта рассмотрим на примере:

Дан текст. Нужно составить список всех встречающихся в тексте слов, упорядоченный сначала по возрастанию длины слова, а потом лексикографически. Удивительно, но данную задачу можно решить совсем не используя ThenBy.

Наивное решение заключается в том, чтобы сформировать нужные кортежи, отсортировать их, а затем извлечь вторые компоненты кортежей, содержащих решение:

1
2
3
4
5
6
7
8
9
10
public static List<string>  GetSortedWords(string text)
{
return Regex.Split(text, @"\W+")
.Where(x => x.Length > 0)
.Select(t => Tuple.Create(t.Length, t.ToLowerInvariant()))
.OrderBy(x => x)
.Select(x => x.Item2)
.Distinct()
.ToList();
}

Однако следует напомнить, что keySelector функции OrderBy — это функция, которая каждому элементу последовательности ставит в соответствие некоторый ключ, по которому его будут сравнивать при сортировке. Поэтому решение можно записать намного короче, поместив кортеж (типа ValueTuple) в keySelector функции OrderBy:

1
2
3
4
5
6
7
8
public static List<string>  GetSortedWords(string text)
{
return Regex.Split(text.ToLower(), @"\W+")
.Where(x => !string.IsNullOrEmpty(x))
.OrderBy(x => (x.Length, x))
.Distinct()
.ToList();
}

Вывод программы можно увидеть ниже:

1
2
3
4
5
GetSortedWords("A box of biscuits, a box of mixed biscuits, and a biscuit mixer.")
'a' 'of' 'and' 'box' 'mixed' 'mixer' 'biscuit' 'biscuits'

GetSortedWords("Each Easter Eddie eats eighty Easter eggs.")
'each' 'eats' 'eggs' 'eddie' 'easter' 'eighty'

LINQ оператор OrderBy

Для сортировки последовательности в LINQ имеется четыре метода:

1
2
3
4
5
IOrderedEnumerable<T> OrderBy<T>(this IEnumerable<T> items, Func<T, K> keySelector);
IOrderedEnumerable<T> OrderByDescending<T>(this IEnumerable<T> items, Func<T, K> keySelector);

IOrderedEnumerable<T> ThenBy<T>(this IOrderedEnumerable<T> items, Func<T, K> keySelector);
IOrderedEnumerable<T> ThenByDescending<T>(this IOrderedEnumerable<T> items, Func<T, K> keySelector);

Первые два дают на выходе последовательность, упорядоченную по возрастанию/убыванию ключей. А keySelector — это как раз функция, которая каждому элементу последовательности ставит в соответствие некоторый ключ, по которому его будут сравнивать при сортировке.

1
2
3
var names = new[] { "Pavel", "Alexander", "Anna" };
IOrderedEnumerable<string> sorted = names.OrderBy(n => n.Length);
Assert.That(sorted, Is.EqualTo(new[] { "Anna", "Pavel", "Alexander" }));

Если при равенстве ключей вы хотите отсортировать элементы по другому критерию, то на помощь приходит метод ThenBy.

Например, в следующем примере все имена сортируются по убыванию длин, а при равных длинах — лексикографически.

1
2
3
4
5
6
7
var names = new[] { "Pavel", "Alexander", "Irina" };

var sorted = names
.OrderByDescending(name => name.Length)
.ThenBy(n => n);

Assert.That(sorted, Is.EqualTo(new[] { "Alexander", "Irina", "Pavel" }).AsCollection);

Чтобы убрать из последовательности все повторяющиеся элементы используют LINQ функцию Distinct:

1
2
3
var numbers = new[] { 1, 2, 3, 3, 1, 1, };
var uniqueNumbers = numbers.Distinct();
Assert.That(uniqueNumbers, Is.EqualTo(new[] { 1, 2, 3 }).AsCollection));

LINQ оператор SelectMany

В LINQ есть такой необычный оператор, как SelectMany. В чем же его необычность?

Рассмотрим сигнатуру метода:

1
IEnumerable<R> SelectMany(this IEnumerable<T> items, Func<T, IEnumerable<R>> f)

Из сигнатуры метода SelectMany видно, что он применяется к последовательности типа IEnumerable<T>. К каждому элементу этой последовательности применяется функция Func<T, IEnumerable<R>>, которая преобразует элемент типа T в последовательность типа IEnumerable<R>. И на выходе мы имеем последовательность типа IEnumerable<R>. Получается, что каждый элемент превращается в множество, которое может быть закрыто отличным типом от исходного, которое затем спрямляется. Результатом работы SelectMany является конкатенация всех полученных последовательностей, т.е. мы имеем не последовательность последовательностей, а единую последовательность типа, отличного от исходного (не обязательно отличного, можно получить и последовательность того же типа).

Поясним вышесказанное следующим примером:

1
2
3
string[] words = {"ab", "", "c", "de"};
IEnumerable<char> letters = words.SelectMany(w => w.ToCharArray());
Assert.That(letters, Is.EqualTo(new[] {'a', 'b', 'c', 'd', 'e'}));

Впрочем строка уже сама по себе является последовательностью символов и реализует интерфейс IEnumerable, поэтому вызов ToCharArray на самом деле лишний.

Одно из не совсем очевидных применений SelectMany — это вычисление декартова произведения двух множеств. Декартово произведение множества {-1, 0, 1} на само себя даст все возможные относительные координаты соседей условной точки Point, где Point – класс, имеющий координаты X и Y в качестве открытых полей. Для вычисления декартова произведения двух множеств также потребуется использовать метод Select внутри SelectMany, как показано в примере ниже:

1
2
3
4
5
public static IEnumerable<Point> GetNeighbours(Point p)
{
int[] d = {-1, 0, 1};
return d.SelectMany(i => d.Select(s => new Point(p.X + i, p.Y + s)));
}

Вот ещё один интересный пример использования SelectMany. Требуется составить лексикографически упорядоченный список всех встречающихся слов в массиве строк. Слова нужно сравнивать регистронезависимо и выводить в нижнем регистре.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void Main()
{
var vocabulary = GetSortedWords(
"Hello, hello, hello, how low",
"",
"With the lights out, it's less dangerous",
"Here we are now; entertain us",
"I feel stupid and contagious",
"Here we are now; entertain us",
"A mulatto, an albino, a mosquito, my libido...",
"Yeah, hey"
);
foreach (var word in vocabulary)
Console.WriteLine(word);
}

public static string[] GetSortedWords(params string[] textLines)
{
return textLines
.SelectMany(x => Regex.Split(x.ToLower(), @"\W+"))
.Where(x => x.Length > 0)
.Distinct()
.OrderBy(x => x)
.ToArray();
}

Здесь для разбиения строки на слова используется класс Regex из пространства имён System.Text.RegularExpressions. Все полученные слова “спрямляются” (объединяются из разных массивов в один массив), удаляются пустые строки и повторы. В конце массив лексикографически упорядочивается и возвращается.

Без использования SelectMany пришлось бы очень трудно, т.к. каждая входящая строка после разбиения Regex.Split превращается в массив слов и мы бы получили массив массивов.

Вывод программы можно увидеть ниже:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
a
albino
an
and
are
contagious
dangerous
entertain
feel
hello
here
hey
how
i
it
less
libido
lights
low
mosquito
mulatto
my
now
out
s
stupid
the
us
we
with
yeah

Как преобразовать int в Guid заданным способом

Потребовалось давеча в рамках тестирования формировать Guid таким образом, чтобы он генерировался не случайно, а нужным мне управляемым способом. А именно так, чтобы при использовании цикла for по int формировался Guid следующим образом:

для int = 1 получить Guid = ‘00000000-0000-0000-0000-000000000001’
для int = 2 получить Guid = ‘00000000-0000-0000-0000-000000000002’
для int = 3 получить Guid = ‘00000000-0000-0000-0000-000000000003’

и так далее…

Как этого добиться? Решение данной задачи представлено ниже:

1
2
3
4
5
6
7
public static Guid ToGuid(int value)
{
var bytes = new byte[16];
BitConverter.GetBytes(value).CopyTo(bytes, 0);
bytes = bytes.Reverse().ToArray();
return new Guid(bytes);
}

Сопоставление с образцом на основе значений свойств объекта

Рассмотрим класс комнаты и перечисление статуса члена клуба такого вида:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Room
{
public int Number { get; set; }
public string Type { get; set; }
public string Size { get; set; }
}

public enum Membership
{
Bronze,
Silver,
Gold
}

Метод GetRooms возвращает список комнат вида:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static List<Room> GetRooms()
{
return new List<Room>
{
new Room
{
Number = 3,
Size = "Large",
Type = "Deluxe"
},
new Room
{
Number = 2,
Size = "Large",
Type = "Regular"
},
new Room
{
Number = 1,
Size = "Standard",
Type = "Regular"
}
};
}

Начиная с C# 8.0 появилась возможность сопоставлять с образцом в выражении switch, используя несколько значений свойств объекта класса:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const int RoomNotAvailable = -1;

public static int AssignRoom(Membership membership)
{
foreach(var room in GetRooms())
{
Membership clientCategory = room switch
{
{ Size: "Large", Type: "Deluxe" } => Membership.Gold,
{ Size: "Large", Type: "Regular" } => Membership.Silver,
{ Size: "Standard", Type: "Regular" } => Membership.Bronze,
_ => Membership.Bronze
};

if (clientCategory == membership)
return room.Number;
}

return RoomNotAvailable;
}

Код ветки _ => Membership.Bronze аналогичен случаю default: в стандартном switch.

Тогда основной метод программы может использовать вышеприведённый код таким образом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static voir Main()
{
Console.Write("Choose status: 0 for Bronze, 1 for Silver, 2 for Gold");
string clientStatus = Console.ReadLine();

Enum.TryParse(clientStatus, out Membership membership);

int roomNumber = AssignRoom(membership);

if (roomNumber == RoomNotAvailable)
Console.WriteLine("Room not available");
else
Console.WriteLine($"Room number is {roomNumber}");
}

Основная магия сопоставления с образцом с использованием значений свойств объектов происходит в методе AssignRoom. Раньше можно было делать switch только на основе единственного значения, но начиная с С# 8.0 паттерн матчинг можно делать на основе значений нескольких свойств экзмепляров классов. Такой подход намного более читабельный и улучшает ясность кода.

DateTime.Now vs. Stopwatch

Читая книгу Андрея Акиньшина “Профессиональный бенчмарк” решил проверять приводимые в книге примеры на практике (автор сам так рекомендует). Начал, конечно же, со столь любимого мною в прошлом “бенчмарка новичков”: измерения DateTime.Now до и после испытуемого на скорость кода и последующего вычисления разности.

Но вот в чем засада по словам автора: для Windows10 частота обновления DateTime по умолчанию 64Гц. А это значит, что новое значение получается с интервалами 15,625 мс и это же значение составляет точность подхода с DateTime. Т.е для всех операций, которые выполнятся быстрее 15,625 мс мы получим значение времени выполнения интересующего кода равное 0, либо, если не повезёт, то 15,625 мс. Такая точность не всегда бывает недопустима, особенно если испытуемый на скорость код выполняется быстрее.

1
2
3
4
5
6
var list = Enumerable.Range(0, 10000).ToList();
DateTime start = DateTime.Now;
list.Sort();
DateTime end = DateTime.Now;
TimeSpan elapsedTime = end - start;
Console.WriteLine(elapsedTime.TotalMilliseconds);

Пробую код из книги на Windows10 C#6.0 и получаю результаты около 4,5 мс. Предсказанный Андреем 0 никак получить не удалось, как и собственно 15,625 мс.

Двигаемся дальше.

По словам автора, лучшей альтернативой DateTime.Now является класс System.Diagnostics.Stopwatch. Типичное разрешение Stopwatch в Windows10 составляет величину порядка 300-500 нс.

1
2
3
4
5
6
var list = Enumerable.Range(0, 10000).ToList();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
list.Sort();
stopwatch.Stop();
TimeSpan elapsedTime = stopwatch.Elapsed;
Console.WriteLine(elapsedTime.TotalMilliseconds);

Этот код на машине автора выдавал результат в 0,05 мс. Однако на моей машине средний результат крутился в районе 0,491 мс, что в 10 раз больше, чем ожидалось!

Никакого анализа причин этих цифр пока привести не могу. Только начинаю погружаться в тонкости бенчмарка. Однако отмечу, что поведение DateTime.Now на моей машине много точнее, чем на машине автора, а System.Diagnostics.Stopwatch на моей машине на порядок хуже, чем на машине автора. В целом нужно согласиться с автором, что Stopwatch точнее (в моей случае на порядок), чем DateTime.Now.

Также следует иметь ввиду, что кроме низкого разрешения DateTime.Now работает медленнее DateTime.UtcNow, т.к. он пересчитывает часовые пояса. Соглашусь с советом Андрея Акиньшина, что в 99% случаев следует выбирать Stopwatch, а не DateTime. DateTime может потребоваться только в случае, если вам действительно нужно знать текущее время. Если реальное время не требуется, то используйте Stopwatch.

Особенность отображения Swagger'ом XML комментариев

Swashbuckle Swagger – это фреймворк для спецификаций RESTful API, которые выраженны с помощью JSON. Swagger используется вместе с набором программных инструментов с открытым исходным кодом для проектирования, создания, документирования и использования веб-служб RESTful. Его сильная сторона заключается в том, что он дает возможность не только интерактивно просматривать спецификацию контроллеров WebAPI, но и отправлять запросы через Swagger UI.

Swagger – удобный инструмент для реализации встроенной в код документации. Добавление XML-комментариев методам контроллеров делает описание методов наглядными при работе через Swagger UI. Отличное руководство по Началу работы с Swashbuckle и ASP.NET Core расположено на сайте Майкрософт. Там приведены рекомендации по подключению Swagger и по оформлению XML-комментариев к методам контроллеров WebAPI.

Рассмотрим пример одного из таких XML-комментариев метода Create:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item #1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(TodoItem item)
{
_context.TodoItems.Add(item);
await _context.SaveChangesAsync();

return CreatedAtAction(nameof(Get), new { id = item.Id }, item);
}

Обратите внимание, как эти дополнительные комментарии улучшают пользовательский интерфейс:

Однако я столкнулся со следующей проблемой: в случае присутствия одного из следующих символов: & > < “ ‘ в XML-комментарии, описание метода в Swagger UI полностью ломается и перестаёт отображаться, т.е. метод начинает выглядеть так, как будто никакого XML-комментария вовсе и нет.

Проблема заключается в природе самого XML, который по своей сути знает только об &amp; &lt; &gt; &apos; &quot;, а также числовых объектах. Поэтому вместо & > < ‘ “ нужно использовать &amp; &lt; &gt; &apos; &quot;. В таком случае комментарии к методам WebAPI исчезать из Swagger UI не будут.