Hurtowe dodanie strict_types=1

Jednolinijkowiec na dziś:

grep -Z -r -L --include \*.php -P "strict_types=1" src/ | xargs -0 -l sed -i 's/<?php/<?php\n\ndeclare(strict_types=1);/g'

To polecenie wyszuka wszystkie pliki z rozszerzeniem php, które nie zawierają ciągu strict_types=1, a następnie doda declare(strict_types=1); zaraz po otwierającym znaczniku <?php. Żeby tylko wyszukać pliki, które nie mają strict_types=1 można użyć:

grep -r -L --include \*.php -P "strict_types=1" src/

Taki automat może się przydać przy modernizacji starszej aplikacji, warto jednak mieć dobre testy, bo taka trywialna zmiana może skutecznie ją wyłożyć.

UPSERT wielu wierszy w MySQL

Dzisiaj zacznę odwrotnie niż zwykle czyli od prezentacji gotowego rozwiązania:

INSERT INTO user_counters (user_id, counter_type, counter_value, updated_at)
VALUES
    (:user_id1, :counter_type1, :counter_value1, :updated_at1),
    (:user_id2, :counter_type2, :counter_value2, :updated_at2),
    # ...
    (:user_idN, :counter_typeN, :counter_valueN, :updated_atN)
ON DUPLICATE KEY UPDATE
    counter_value = counter_value + VALUES(counter_value),
    updated_at = VALUES(updated_at);

To tak zwany UPSERT czyli zapytanie, które wstawia rekord(y) do bazy, a jeśli taki już istnieje (tzn. istnieje już taki klucz główny lub unikalny) to go zaktualizuje (wartość w kolumnie counter_value zostanie powiększona o nową wartość, a w kolumnie updated_at zastąpiona). To zapytanie jest jednak trochę lepsze od tych, które zazwyczaj podaje się jako przykład UPSERTA, ponieważ pozwala zapisać wiele (dziesięć, sto, tysiąc) wierszy za jednym zamachem. Sprawia to użyta w części UPDATE funkcja VALUES(), która pobiera wartości ze wskazanej kolumny w części INSERT.

Po co się w ogóle tak babrać w SQL-u jak zwierzę skoro Doctrine mógłby to zrobić za nas? Akurat tego nie zrobi. Przy użyciu Doctrine należałoby:

  1. Pobrać wszystkie pasujące rekordy (jeden SELECT).
  2. Przeiterować przez nie i:
    • zaktualizować wartości,
    • zidentyfikować brakujące rekordy.
  3. Utworzyć encje brakujących rekordów.
  4. Opróżnić bufor managera encji (flush, który zapewne wykona jeden INSERT i jeden UPDATE).

Jak widać zarówno po stronie aplikacji (PHP) jak i bazy to wyraźnie więcej pracy, która i tak pewnie poszłaby na marne, bo przy kilku pracujących jednocześnie instancjach aplikacji jest spora szansa na wyjątek UniqueConstraintViolationException, który zamknie managera encji.

UPSERT z wieloma wartościami rozwiązuje problem wydajności i problem naruszania ograniczeń, ale tworzy klikna innych:

  • Przy jednoczesnych instancjach aplikacji łatwo o deadlocki na bazie. Trzeba zastosować jakiś mechanizm blokowania.
  • Jeśli w ten sposób zapisujemy dane z encji Doctrine’a to ich stan może być nieaktualny.

INSERT … ON DUPLICATE KEY UPDATE nie jest częścią standardu SQL, to rozszerzenie dostępne w MySQL. W PostgrSQL jest podobny mechanizm, ale jego działanie jest nieco inne, więc może opiszę je przy innej okazji.

Dependency injection uzależnione od routingu

Żeby w kontenerze Symfony uzależnić rodzaj wstrzykiwanego obiektu od akcji, która jest wykonana można posłużyć się następującym wyrażeniem:

app.some_service:
    class: App\Service\SomeService
    arguments:
        - "@=service(container.get('request_stack').getCurrentRequest() !== null and container.get('request_stack').getCurrentRequest().get('_route') == 'app_some_route' ? 'app.dependency.specific’ : 'app.dependency.generic’)"

Tak skonfigurowana usługa app.some_service zostanie zbudowana z zależnością app.dependency.specific jeśli aktualna trasa (route) to app_some_route, a jeśli inna to zostanie wstrzyknięta app.dependency.generic. W praktyce można to wykorzystać np. do wstrzyknięcia repozytorium bazodanowego w dla akcji z panelu administracyjnego (gdzie potrzebna jest aktualna wersja danych) lub repozytorium czerpiącego dane z cache dla prezentacji danych użytkownikom.

Z drugiej strony taka definicja usługi jest zagmatwana i może utrudniać debugowanie, więc pewnie postarałbym się inaczej rozwiązać taki problem.

Zduplikowana nazwa grupy woluminów LVM

Podczas przenoszenia systemu na nowy dysk zafundowałem sobie mały problem: nazwałem grupę woluminów LVM tak samo jak na starym dysku. Kiedy do świeżo postawionego systemu podłączyłem stary dysk, żeby skopiować dane ów problem objawił się tak:

# vgscan
  Reading volume groups from cache.
  Found volume group "cherokee" using metadata type lvm2
  Found volume group "cherokee" using metadata type lvm2

Jak wiadomo, żeby aktywować LVM trzeba podać nazwę grupy woluminów, a skoro ta jest zduplikowana to jest to problem. Jego rozwiązaniem jest zmiana nazwy jednej z grup. Ja postanowiłem zmienić nazwę starej grupy.
Czytaj dalej „Zduplikowana nazwa grupy woluminów LVM”

Monitor nie wybudza się z uśpienia

Pracuję na laptopie Dell Latitude E5450 z replikatorem portów Dell Advanced E-Port II oraz z dwoma monitorami Dell U2515H podłączonymi przez DisplayPort. Na co dzień taki zestaw spisuje się dobrze, ale przez długi czas trapiła mnie uciążliwa usterka. Mianowicie po zablokowaniu/uśpieniu laptopa, co powoduje wygaszenie wszystkich ekranów, a następnie odblokowaniu/wybudzeniu dodatkowe monitory pozostawały wygaszone. W systemie były widoczne, w ustawieniach ekranu były na swoich miejscach, ale nie wyświetlały obrazu.

Próbowałem różnych rozwiązań: połączenia przez HDMI, użycia sterowników NVidii zamiast nouveau, ale okazały się nieskuteczne. Doraźnym rozwiązaniem była zmiana rozdzielczości obrazu albo wzajemnego położenia ekranów.

Skuteczne rozwiązanie podpowiedział mi kolega z pracy. Jest nim przestawienie w menu monitora na użycie DisplayPort 1.2. Po wykonaniu tej operacji od kilku dni problem nie występuje.

Przed wykonaniem tej operacji należy upewnić się, że sprzęt i system obsługują DisplayPort. Kolega, który pracuje na identycznym zestawie, za moją radą przestawił monitor na DisplayPort 1.2 i monitor przestał wyświetlać obraz, w OSD nawet nie można było wybrać menu, w którym jest opcja przestawienia wersji DisplayPort. Żeby to odkręcić musieliśmy podpiąć jego monitor pod mój komputer.

Migracja repozytorium SVN do git

Młodsi stażem programiści pewnie tego nie pamiętają, ale kiedyś synonimem systemu kontroli wersji był Subversion. W dziewięciu na dziesięć przypadków podczas rozmowy kwalifikacyjnej należało się wykazać znajomością SVN-a.

Z tamtych czasów zostało mi kilka projektów w SVN-ie. Większość z nich już od dawna nie funkcjonuje, ale kilka wciąż żyje, np. acp czy ta strona. Raz na ruski rok coś w nich poprawiam, choć ze wstydem przyznaję, że robiłem to wprost na serwerze, bo przy wymianie domowego serwerka parę lat temu już nawet nie stawiałem serwera Subversion. Teraz jednak postanowiłem zrobić z tym porządek i przenieść kod do gita.

Czytaj dalej „Migracja repozytorium SVN do git”

Ansible i cache buster

Pamięć podręczna przeglądarki może napsuć krwi programistom zajmującym się frontem. Wielokrotnie byłem świadkiem podobnych dialogów:
– Frontend developerze, mówiłeś, że to naprawiłeś, a nie działa.
– Naciśnij Control i F5.
– Aaa, OK.
Żeby wymusić na przeglądarce pobranie nowej wersji plików (JavaScriptów, arkuszy stylów) można do nazwy pliku dodać query string z wartością zmienianą razem plikami czyli tzw. cache buster, na przykład:

<link rel="stylesheet" type="text/css" href="compiled.css?v=1.23">

Mniej więcej tak robiliśmy w aplikacji, z którą obecnie dużo pracuję. Występujące w przykładzie 1.23 to była wersja aplikacji. Wartość ta była pobierana z pliku konfiguracyjnego. To rozwiązanie miało przynajmniej dwie wady:

  1. Na środowiska testowe często wydajemy wielokrotnie kolejne poprawki do tej samej wersji aplikacji, przez co cache buster nie działał.
  2. Zdarzało nam się zapomnieć podbić wersję w pliku konfiguracyjnym, przez co cache buster nie działał także na produkcji.

Ponieważ do wydań używamy ansible’a postanowiliśmy zautomatyzować generowanie wartości dla cache bustera.

W pierwszym podejściu chciałem użyć daty i czasu wykonania skryptu np. w formacie RRMMDDGGMMSS, ale to rozwiązanie nie za dobrze działa w wypadku, gdy wydanie jest robione dla podzbioru serwerów (tzw. rolling update). Czas uruchomienia skryptu jest inny dla każdej grupy serwerów.

Lepszym rozwiązaniem moim zdaniem jest użycie ID commitu z gita. Zalety tego podejścia to:

  1. Wartość będzie identyczna na każdym serwerze, niezależnie od tego w której serii serwer był aktualizowany.
  2. Wartość jest generowana automatycznie, więc odpada czynnik ludzko-zapominalski.
  3. Wartość jest inna po każdej zmianie kodu w repozytorium, więc będzie różna również dla każdej poprawki w ramach jednej wersji aplikacji.

W roli ansible’a można to ograć następująco:

- name: "Get git short commit ID"
  command: git rev-parse --short HEAD
  args:
    chdir: "/tmp/build-directory"
  register: git_revparse

W powyższym przykładzie wartość chdir to katalog, w którym mamy kod aplikacji z repozytorium git. Po wykonaniu tego kroku w
w zmiennej git_revparse.stdout będzie znajdować ID commitu, które można przekazać np. do szablonu pliku konfiguracyjnego.

Obecnie linki do skryptów i styli wyglądają mniej więcej tak:

<link rel="stylesheet" type="text/css" href="compiled.css?v=c109ad9">