Pobieranie nowych wiadomości przez JavaMail

I tylko nowych.

Stanąłem przed problemem jak efektywnie pobierać maile ze skrzynek POP3 za pomocą JavaMail w przypadku, gdy pobrane wiadomości nie są usuwane z serwera. Oczywiście głównym problemem jest wygenerowanie listy wiadomości, które znajdują się w skrzynce odbiorczej konta POP3, a nie znajdują się jeszcze w lokalnej bazie.

Do tej pory w Funneli stosowaliśmy prosty i, wydawałoby się, efektywny algorytm:

  1. Sprawdź kiedy pobrano ostatnią wiadomość z tego konta
  2. Wyszukaj nowsze wiadomości (uwzględniając kilkugodzinny margines)
  3. Pobierz wiadomości pomijając już pobrane

W Javie wyglądało to mniej więcej tak:

folder.open( Folder.READ_WRITE );
Date d = ... //tu pobranie z bazy daty ostatnio odebranego maila
SentDateTerm onlyRecent = new SentDateTerm( DateTerm.GE, d );
Message[] msgs = folder.search( onlyRecent ); //wyszukanie tylko nowszych, które pobiera pełne nagłówki - tu jest problem
for ( Message m: msgs ){
	com.sun.mail.pop3.POP3Folder pf = (com.sun.mail.pop3.POP3Folder)folder;
	String uid = pf.getUID( m );
	bool exists = ... //sprawdzenie czy uid już jest w bazie
	if ( ! exists ){
		//pobranie maila i zapis do bazy
	}
}

Okazuje się, że JavaMail, aby móc porównać daty, pobiera całe nagłówki wszystkich maili. W sieci znalazłem informację, ze średnio nagłówek zajmuje ok 1kB (nie sprawdzałem). Przy skrzynce z 5000 maili daje to jakieś 5MB do pobrania… co 30 sekund.
Na szczęście istnieje bardziej efektywny sposób – pobrać identyfikatory wszystkich maili za pomocą metody fetch() i nie wykonywać punktu 2, czyli porównywania dat:

folder.open( Folder.READ_WRITE );
Message[] msgs = folder.getMessages();
FetchProfile fp = new FetchProfile();
fp.add( UIDFolder.FetchProfileItem.UID );
folder.fetch( msgs, fp ); //wstępne pobranie identyfikatorów
for ( Message m: msgs ){
	com.sun.mail.pop3.POP3Folder pf = (com.sun.mail.pop3.POP3Folder)folder;
	String uid = pf.getUID( m );
	bool exists = ... //sprawdzenie czy uid już jest w bazie
	if ( ! exists ){
		//pobranie maila i zapis do bazy
	}
}

Na pierwszy rzut oka takie rozwiązanie wydaje się mniej efektywne. W końcu pobieramy pełną listę wiadomości i po kolei, dla każdej pozycji, sprawdzamy czy nie została już pobrana. W praktyce okazuje się, że różnica w ilości przesyłanych danych w przypadku gdy pobieramy całe nagłówki do wyszukiwania po dacie, a sytuacji gdy pobierane są jedynie unikalne identyfikatory, jest tak ogromna, że czas sprawdzania czy wiadomość znajduje się w lokalnej bazie jest praktycznie pomijalny.

W przeprowadzonych testach w skrajnych przypadkach zysk czasu był tysiąckrotny.

Java (od Apple): zaskakujące update’y

Poniższy problem starych wyjadaczy Javy pewnie nie zaskoczy, ale ja ani za Javą nie przepadam, ani jej zbyt często nie używam, więc lekko mnie zdziwił…

Dzisiaj postanowiłem poprawić drobny błąd przy konwertowaniu maili z HTML na czysty tekst. Poprawa trywialna: kilka wywołań replaceAll() i po kłopocie. Ale…

Po wrzuceniu poprawki na produkcję, przy wysyłaniu maili, pojawił się błąd:

javax.mail.MessagingException: Could not connect to SMTP host: xxxxxxxxxx, port: 25;
nested exception is:
java.net.SocketException: java.lang.ClassNotFoundException: Cannot find the specified class aaaaa.yyyyy

Błąd oczywiście zupełnie nie związany z wprowadzonymi poprawkami.
Okazało się, że ostatnia (zaintalowałem ją wczoraj) poprawka Javy od Apple zmienia nieco zachowanie kompilatora. Klasa potrzebna do zestawienia połączenia SSL (wyżej nazwana aaaaa.yyyyy) jest ładowana dynamicznie przez wskazanie jej jako dostawcy gniazda przy połączeniach SSL:

Security.setProperty( “ssl.SocketFactory.provider”, SSL_PROVIDER );

Pomimo iż w nagłówku pliku z klasą gdzie ww instrukcja się pojawiła widniała dyrektywa:

import aaaaa.*;

Kompilator postanowił nie kompilować potrzebnej klasy, bo nie była ona jawnie użyta. Wcześniej nie było tego problemu i wszystkie klasy wpisane w import były kompilowane, niezależnie czy kompilator wykrył, że są używane, czy też nie.

Rozwiązanie jest oczywiście trywialne – nie należy polegać na wykrywaniu zależności przez kompilator i trzeba ręcznie (jawnie w skrypcie kompilacji) skompilować potrzebne klasy.

IMHO to dość istotna zmiana w zachowaniu kompilatora jak na poprawkę nie zmieniającą nawet numeru wersji (nazwali ją Java for Max OS X 10.6 Update 1)

To nie pierwszy raz, kiedy Apple zaskoczyło mnie poprawką Javy, która spowodowała problemy z naszym systemem.

Hi, I’m not a PC. Na szczęście.

Disclaimer: Nie chcę tutaj rozpętać jakiegoś flamewara. Pomimo, że jestem fanem Apple to nigdy na siłę nie udowadniałem nikomu wyższości rozwiązań tej firmy (szczególnie jeśli chodzi o sprzęt). Jak ktoś woli PC to szanuję jego wybór. Oczywiście dopóki nie każe mi przy nim pracować. Wpis dotyczy sprzętu, a od jakiegoś czasu Maci to zwykłe PC-ty, tylko w ładniejszej obudowie i z mniejszą liczbą portów.

Właśnie na gazeta.pl znalazłem informację o rankingu awaryjności komputerów. Według tego rankingu Apple to najbardziej niezawodne komputery.

Od 5 lat jestem użytkownikiem komputerów Apple. Właściwie to notebooków, z komputerami stacjonarnymi większej styczności nie miałem. Pozwólcie, że przytoczę historię (z punktu widzenia awaryjności) moich komputerów tej firmy:


Rozdział pierwszy: listopad 2004 – wrzesień 2006, iBook G4 12″


Pierwszy komputer Apple w moich rękach i zaraz pierwszy notebook w moim posiadaniu. Dlaczego zdecydowałem się na Apple, pomimo że nigdy komputerów tej firmy nie używałem, pozwolę sobie pominąć. Najbardziej niezawodny notebook jakiego miałem. Do czasu.

1. kilka (5 jeśli dobrze pamiętam) dni przed końcem 12-miesiecznej gwarancji katastrofa – martwa płyta główna. Czas oczekiwania na nową: prawie miesiąc.

2. komputer wraca, ale źle złożony. Ponieważ wtedy naprawy gwarancyjne komputerów Apple serwisował tylko SAD w Warszawie, aby nie tracić czasy problem postanowiłem rozwiązać sam

3. prawie po roku od wymiany płyty déjà vu – martwa płyta. Niestety gwarancja już dawno minęła.

Po drodze komputer załapał się na program wymiany baterii. Stara bateria była sprawna do ostatnich dni (tak, nie odesłałem jej – przepraszam).

Dwa lata używania to akurat dobry moment na update sprzętu. Apple akurat przeszedł na procesory Intela, więc i tak czas pozbyć się architektury PowerPC z biurka. Co prawda do teraz uważam, że PowerPC był lepszy niż x86, ale to temat na oddzielny wpis.


Rozdział drugi: od września 2006, MacBook White CoreDuo


Problemy dotyczące wszystkich wczesnych MacBooków nie mogły mnie ominąć:

1. mrugająca matryca – problem rozwiązałem dopiero niedawno przyklejając kabelek zasilający do inwertera

2. żółknąca i pękająca obudowa – no comment

A dalej:

3. padł napęd DVD. Ok, nie używałem go zbyt intensywnie więc żadna strata :)

4. zasilacz. Najpierw wtyczka sama nie wiedziała czy chce świecić czy nie i czasami podłączała się tak, że bateria się nie ładowała (prawdopodobnie coś z tymi bolcami w MagSafe było nie tak). Potem pękł kabelek przy samej obudowie zasilacza – nie mam pojęcia jak to się stało.

5. bateria. W dwa lata zajechałem dwie. Te w iBooku były zdecydowanie lepsze. Jacek z resztą też wymieniał baterię w MacBooku, więc coś musi być na rzeczy

I tak sukces: komputer, pomimo zalania kawą i koniecznością wymiany klawiatury, działa do dzisiaj (wykluczając DVD i słabą baterię – trzeciej nie kupiłem)


Rozdział trzeci: od listopada 2008, MacBook Pro Unibody 15.4″


Do trzech razy sztuka, cza na (naj)wyższą półkę. Zamawiam mojego MBP na długo zanim kontener przypływa do Polski. W zakupie pomaga mi PFRON – polecam :)

Teraz też pojawiają sie problemy wieku niemowlęcego:

1. już w pierwszych dniach zauważam coś niepokojącego: notebook budzi się samoczynnie.

2. touchpad ma humory: czasami kliknięcie działa, czasami nie

Na szczęście przyczyną obu problemów był firmware. Dwa update’y i problemy znikają. Ale:

3. bardzo ciekawie działa układ sterujący wentylatorami. Notebook jest cichy, czasami za bardzo. Szczególnie wtedy gdy temperatura wynosi prawie 100 stopni… Mój prywatny rekord: 105 stopni. Problem pojawia się na szczęscie tylko przy dużym obciążeniu karty graficznej. Rozwiązanie: smcFanControl i ręczne ustawienie wentylatorów na maxa przed włączeniem Call of Duty 4

Ok, powyższe problemy w sumie nie są uciążliwe lub zostały szybko rozwiązanie. Teraz ciekawsze:

4. Pewnego dnia otworzyłem notebooka i ujrzałem coś takiego:

Awaria matrycy

Przy poruszaniu czasami udawało się zmusić matrycę do działania – oczywista diagnoza: złamany kabelek.

Zawiozłem notebooka do serwisu firmy Cortland w Poznaniu. Szybka diagnoza (taka sama jak moja) i informacja: ściągamy nową matrycę. Tak, ktoś nie przewidział, że kabelek idący przez otwierany kilka razy dzienne zawias może się złamać i postanowił go zakleić wewnątrz matrycy. Super! Na szczęście DisplayPort działa bez zarzutu. Biorę notebooka do domu i działam na zewnętrznym monitorze. Po paru dniach przybywa matryca. Szybka wymiana i… nie działa AirPort. A właściwie to został wyjęty i nie włożony z powrotem. Tydzień czekania, AirPort wraca na miejsce. Jeszcze dwie wycieczki do Cortlandu bo komputer był źle złożony: brak podświetlenia matrycy i odstający plastik oraz ostatnia naprawa korespondencyjna – pan Rafał (pozdrawiam!) zapomniał wkręcić dwóch śrubek w obudowę. Nie chciałem już go nachodzić, więc poprosiłem o wysłanie tych śrubek pocztą.

5. Na koniec jeszcze ciekawostka: chipset nVidii nie obsługuje niektórych (tak, tych bardziej popularnych) kości RAM, gdy wsadzimy dwie po 2GB. Ok, jest lista pamięci kompatybilnych i te działają, nie można więc nazwać tego usterką. Ale sorry, od czegoś są standardy i wypadałoby żeby je stosować…

Z przezorności przedłużyłem gwarancję do 3 lat. Lepiej dmuchać na zimne (o ile tak można powiedzieć o czymś, co ma temperaturę wrzątku).


Konkluzja


Bardzo cieszę się, że jestem użytkownikiem najmniej awaryjnego sprzętu (według mnie i tak zbyt awaryjnego) i bardzo serdecznie gratuluję Apple zajęcia tak wysokiej pozycji w rankingu. Jeśli problemów z najmniej awaryjnym sprzętem nie udało mi się zmieścić na jednej stronie A4, to na prawdę bardzo się cieszę, że nie jestem posiadaczem sprzętu firm ze środka, bądź, o zgrozo, z dołu rankingu. Tym bardziej raduje mnie fakt, że nie mam notebooka składanego przez pana Kazia w jego Przedsiębiorstwie Produkcyjno-Handlowym z podzespołów przeznaczonych do komputerów stacjonarnych.

Dlaczego, pomimo licznych problemów, zdecydowałem się pozostać wiernym firmie Apple? Być może dlatego, że oprócz niezawodności komputery cechują się też innymi rzeczami: wydajnością, ergonomią, designem, oprogramowaniem. Szczególnie w trzech ostatnich, IMHO (patrz Disclaimer), Apple jest liderem. Lubię czasami zamknąć mojego MBP, położyć obok otwartego HP, czy Toshiby o podobnych parametrach i bez słowa komentarza porównać ich wysokość… Młotek jest na pewno mniej awaryjny, ale jakoś i tak wolę pracować na Macu.

Edit [2009-12-16]:
Po kilku dniach od opublikowania tego posta, Apple wydało aktualizację EFI dla MBP, której zadaniem było wyciszenie DVD buczącego przy starcie i budzeniu się komputera. Wygląda na to, że ta poprawka rozwiązuje również opisany wyżej problem przegrzewania się. Grejt sukcess!

SSH – ułatwiamy sobie życie

Postanowiłem zebrać dla potomnych (i dla siebie, w razie jakbym czegoś zapomniał) kilka informacji, które ułatwią używanie tego narzędzia oraz pokać inne potencjalne zastosowania inne niż zdalne logowanie.

Automatyczna autoryzacja

Logowanie do zdalnej maszyny jest trywialne:

ssh -llogin nazwa-serwera

potem podajemy hasło i gotowe. Problem pojawia się gdy hasło wygląda tak:

z7kw=TQksHO!CVGSMXmX#5K42hK@ePMQs6(YS

SSH (na szczęście) nie pamięta haseł, ani nie pozwala podawać ich w postaci przełącznika (wtedy można by zapisać hasło w jakimś skrypcie). Jest jednak proste rozwiązania tego problemu – autoryzacja przez parę: klucz prywatny – klucz publiczny.

Cała operacja sprowadza się do trzech kroków: wygenerowania ww. pary, umieszczenia w ustawieniach użytkownika klucza prywatnego i skopiowania na maszynę zdalną klucza prywatnego.

Nie będę przepisywał sczegółowej instrukcji, którą znajdziecie tutaj: http://www.csua.berkeley.edu/~ranga/notes/ssh_nopass.html

Aliasy

Wpisanie nazwy użytkownika i nazwy serwera jako argumentów polecenia nie jest zbyt kłopotliwe, dopóki nie wygląda to tak (przykład skrajny :) ):

ssh -p 1234 -leustachybrzeczyszczykiewicz serwer-osiemnasty.szafa-trzydziestadruga.siec-szkieletowa.funnela.com

Z pomocą przychodzi plik konfiguracyjny, który dostępny jest pod ścieżką ~/.ssh/config (jak go nie ma, to trzeba sobie zrobić).

Można w nim ustawić na prawdę wiele rzeczy (zachęcam do lektury: man ssh_config ), ale nas w tej chwili interesuje najbardziej trywialna: aliasy.

Po dodaniu takiego wpisu do konfiguracji:

Host s18s32

HostName serwer-osiemnasty.szafa-trzydziestadruga.siec-szkieletowa.funnela.com

Port 1234

User eustachybrzeczyszczykiewicz

HostKeyAlias serwer18szafa32

pan Eustachy, aby dostać się na serwer osiemnasty w trzydziestej drugiej szafie wpisuje po prostu:

ssh s18s32

lub

ssh serwer18szafa32

I nie musi już pamiętać nazwy serwera, numeru portu czy nazwy użytkownika. Przede wszystkim nie musi tego za każdym razem wpisywać.

Przekierowywanie portów

Bardzo przydatną w specyficznych sytuacjach funkcją SSH jest możliwość przekierowania portów dowolnej zdalnej maszyny na komputer lokalny. Rozważmy taki przykład:

Nadgorliwy administrator sieci zablokował praktycznie wszystkie usługi. Nawet HTTP kuleje – nie da się wysłać większej porcji danych POST. Na szczęscie działa SSH. (tak jest skonfigurowana sieć Politechniki Poznańskiej).

My chcemy z takiej sieci zrobić commit SVN ( port: 3690 ) lub połączyć się z bazą danych.

Na pomoc przychodzi oczywiście SSH.

Przy okazji połączenia SSH stawiamy tunel, który mapuje porty jakiejś zdalnej maszyny (np. tej na którą logujemy się przez SSH) na porty lokalne.

W tym celu do pliku konfiguracyjnego dopisujemy linijkę w formacie:

LocalForward <port lokalny> <maszyna zdalna>:<port zdalny>

np:

Host tunnel

HostName serwer-pierwszy.funnela.com

User root

LocalForward 3690 localhost:3690

LocalForward 5432 localhost:5432

LocalForward 8080 router.funnela.com:8080

po zestawieniu połączenia ( ssh tunnel ), możemy połączyć się z komputerem lokalnym na porcie 3690 aby uzyskać połączenie z SVN na serwerze pierwszym. Analogicznie dla Postgresa.

Aby nie mieć w programach klienckich dwóch konfiguracji dla połączenia przez tunel ( połączenie z localhost ) i bezpośrednio, można zastosować prosty trick. Po połączeniu tunelu należy dodać do /etc/hosts wpis, który powiąże nazwę serwera ( serwer-pierwszy.funnela.com ) z adresem 127.0.0.1. Od tej chwili wszelkie połączenia SVN lub Postgres z serwer-pierwszy.funnela.com będą szły przez tunel i zgrabnie ominą firewalla.

Podobnie będą działały połączenia z localhost na porcie 8080, jednak tym razem zostaną przekierowane na zupełnie inną maszynę (router).

SOCKS Proxy

W celu ominięcia firewalla lub w przypadku konieczności połączenia się z użyciem innego zewnętrznego adresu IP, możemy również skorzystać z kolejnej przydatnej funkcji SSH – SOCKS proxy. Podczas nawiązywania połączenia ze zdalnym serwerem, możemy użyć przełącznika -D wraz z numerem lokalnego portu. Podczas takiego połączenia nasz komputer lokalny będzie działał jako serwer SOCKS. Wystarczy teraz w ustawieniach programu klienckiego (lub ustawieniach sieci np. w Max OS X) wskazać, żeby połączenie odbywało się przez serwer SOCKS pod adresem localhost na podanym wcześniej porcie. Możemy teraz cieszyć się połączeniem przekierowanym przez nasz serwer SSH.

Serwer proxy jest znacznie prostszy w konfiguracji niż przekierowywanie portów – nie wymaga konfiguracji każdej usługi oddzielnie. Ma jednak sporą wadę: nie wszystkie programy potrafią korzystać z proxy, więc jego zastosowanie ogranicza się głównie do przeglądarki internetowej.

Oczywiście połączenie przez SSH jest zawsze szyfrowane, więc korzystanie z SOCKS lub przekierowywania portów nie tylko pozwala ominąć blokadę portów w sieci, ale również chroni nas przed analizą pakietów przez dynamiczny firewall bądź przechwyceniem ich przez intruza.

SELECT MAX(id) w PostgreSQL

Jak znaleźć najnowszą (czyt. o największym id) krotkę? Wydawałoby się, że nic prostrzego – wystarczy użyć funkcji agregującej MAX():

SELECT MAX( id ) FROM tab_a;

I rzeczywiście w większości przypadków MAX() zdaje egzamin i to bardzo dobrze. Na testowych danych zapytanie wykonywało się 0.05ms. Rozważmy jednak bardziej skomplikowany przypadek:

SELECT MAX( id ) FROM tab_a a JOIN tab_b b ON ( a.xx = b.yy );

I tutaj przestaje być wesoło. Powyższy przykład wykonywał się ponad 22ms, czyli prawie 500 razy dłużej! Oczywiście zarówno id jak i kolumny z warunku połączeniowego mają prawidłowe indeksy. Problem polega na tym, że wynik połączenia zaindeksowany już nie jest i MAX() wykonuje seq scan na tych danych.

Rozwiązanie jest trywialne – wystarczy nie używać MAX():

SELECT id FROM tab_a a JOIN tab_b b ON ( a.xx = b.yy ) ORDER BY id DESC LIMIT 1;

Rezultat: 0.12ms.

Poniżej dowód w postaci screenshotów z “życiowej” bazy (wybaczcie cenzurę – nie chcę ujawniać struktury bazy):