Mateusz Mazurek – programista z pasją

Python, architektura, ciekawostki ze świata IT

Algorytmika Inżynieria oprogramowania Programowanie

Własna baza NoSql – docelowa gramatyka, implementacja parsera i bazy noSql

Cześć,
kontynuując temat rozpoczęty w poprzednim wpisie, dziś pokaże jak wykorzystując antlr’a zbudować własny język którego będzie można użyć do napisania własnej bazy NoSql. Jeśli nie czytałeś poprzedniego wpisu – zapoznaj się z nim koniecznie – link do wpisu.

No to zacznimy… Od początku – czyli genezy tego co napiszemy.
Oprzemy się na już istniejącej bazie NoSql – na Redisie. Pisząc „oprzemy się” mam na myśli że protokół (język) którym będziemy się posługiwać w komunikacji z ta bazą będzie podobny. Ok, zdefiniujmy wymagania funkcjonalne – nasza baza danych powinna zapewnić takie jak:

– zapis/odczyt wartości tekstowej
– zapis/odczyt tablicy
– zapis/odczyt słownika
– sprawdzenie typu klucza
– możliwość usunięcie klucza

plus:

– powinna obsługiwać wiele połączeń jednocześnie
– powinna być dostępna via TCP
– powinna zapewnić atomowość operacji
– powinna zawierać obsługę błędów

Projekt nazwałem dumnie „RedisByMM” :) co nieco przypomina „PhpBB By Przemo” ;)

Jeszcze jedno wtrącenie na temat samych języków – mamy dwa „typy” języków – deklaratywne i imperatywne. Imperatywne języki to takie w których za pomocą składni języka opisuje się proces mający doprowadzić do żądanego efektu – czyli np. Python, C, Java itp.
Deklaratywne języki to języki w których nie opisuje się procesu w którego rezultacie coś ma się wykonać tylko opisujemy co chcemy uzyskać – przykładem jest SQL.

Nasz język będzie deklaratywny a sama gramatyk będzie wyglądać tak:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
grammar redisbymm;

expression
    : (sadd|sdel|sget|ladd|ldel|lget|lgetall|lpop|get|setv|gettype|delk)
    ;
sadd
    : SADD_KEY SPACE key SPACE string_index SPACE val
    ;
SADD_KEY
    : 'SADD'
    ;
sdel
    : SDEL_KEY SPACE key SPACE string_index
    ;
SDEL_KEY
    : 'SDEL'
    ;
sget
    : SGET_KEY SPACE key SPACE string_index
    ;
SGET_KEY
    : 'SGET'
    ;
ladd
    : LADD_KEY SPACE key SPACE val (SPACE val)*
    ;
LADD_KEY
    : 'LADD'
    ;
ldel
    : LDEL_KEY SPACE key SPACE index (SPACE index)*
    ;
LDEL_KEY
    : 'LDEL'
    ;
lget
    : LGET_KEY SPACE key SPACE index
    ;
LGET_KEY:
    'LGET'
    ;
lgetall
    : LGETALL_KEY SPACE key
    ;
LGETALL_KEY
    : 'LGETALL'
    ;
lpop
    : LPOP_KEY SPACE key SPACE index
    ;
LPOP_KEY
    : 'LPOP'
    ;
get
    : GET_KEY SPACE key
    ;
GET_KEY
    : 'GET'
    ;
setv
    : SET_KEY SPACE key SPACE val (SPACE key SPACE val)*
    ;
SET_KEY
    : 'SETV'
    ;
gettype
    : TYPE_KEY SPACE key
    ;
TYPE_KEY
    : 'GETTYPE'
    ;
delk
    : DELK_KEY SPACE key
    ;
DELK_KEY
    : 'DELK'
    ;
index
    : DIGIT+
    ;
DIGIT
    : [0-9]+
    ;
SPACE
    : ' '+
    ;
key
    : (LETTER)+(DIGIT)*
    ;
val
    : (LETTER)*(DIGIT)*
    ;
string_index
    : (LETTER)+(DIGIT)*
    ;
LETTER
    : [a-zA-Z]
    ;

Widzimy że komendy jakie będzie implementować to po prostu:

sadd – dodaj do słownika
sdel – usuń ze słownika
sget – pobierz ze słownika
ladd – dodaj do listy
ldel – usuń z listy
lget – pobierz z listy
lgetall – pobierz całą zawartość listy
lpop – pobierz element z listy i go usuń z niej
get – pobierz wartość typu string
setv – dodaj klucz z wartością string
gettype – sprawdź typ
delk – usuń klucz

Dobra, dość gadania – czas coś napisać.
Zainstalujmy sobie pierw antlr:

1
2
3
4
5
cd /usr/local/lib
sudo curl -O https://www.antlr.org/download/antlr-4.7.1-complete.jar
export CLASSPATH=".:/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH"
alias antlr4='java -jar /usr/local/lib/antlr-4.7.1-complete.jar'
alias grun='java org.antlr.v4.gui.TestRig'

Gdy to już mamy możemy w pliku gramatyki wygenerować sobie parser i lekser. Oczywiście w Pythonie.

Wygenerowało nam to kilka plików:

1
2
3
4
5
6
7
8
9
~/test1 antlr4 -Dlanguage=Python3 redisbymm.g4                            
~/test1 ll                                                                
razem 72K
-rw-r--r--. 1 mmazurek mmazurek 1,3K 09-22 17:32 redisbymm.g4
-rw-r--r--. 1 mmazurek mmazurek 4,1K 09-22 17:33 redisbymmLexer.py
-rw-r--r--. 1 mmazurek mmazurek  280 09-22 17:33 redisbymmLexer.tokens
-rw-r--r--. 1 mmazurek mmazurek 4,8K 09-22 17:33 redisbymmListener.py
-rw-r--r--. 1 mmazurek mmazurek  42K 09-22 17:33 redisbymmParser.py
-rw-r--r--. 1 mmazurek mmazurek  280 09-22 17:33 redisbymm.tokens

Te pliki zapakowałem do folderu lexical_libraries i stworzyłem taką strukturę katalogów (modułów):

Oczywiście venv i venv1 to virtualenvy – po prostu:)

Definiujemy sobie od razu setup.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup, find_packages

requirements = ["antlr4-python3-runtime==4.5.2", "gevent==1.2.2"]

setup(
    name="redisbymm",
    version="0.0.1",
    packages=find_packages("."),
    include_package_data=True,
    entry_points={"console_scripts": ["redisbymm = database.main:run"]},
    install_requires=requirements,
)

deklarując w nim chociażby biblioteki zależne – jedna od używania parsera, leksera i listenera a druga zapewniająca dostęp po TCP używając pętli zdarzeń – antlr4 i gevent. Oczywiście gevent odchodzi trochę w cień kiedy mamy asyncio – ale nadal działa fajnie:)

Zauważ proszę że mamy jeden entrypoint – usytuowany w folderze database, pliku main i funkcji „run”.
Jego zawartość wygląda tak:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from lexical_libraries.redisbymmLexer import redisbymmLexer
from lexical_libraries.redisbymmParser import redisbymmParser
from lexical_libraries import RedisByMMListener, RedisByMMErrorListener
from antlr4 import *
from gevent.server import StreamServer
from gevent.monkey import patch_all

patch_all()


def run():
    def handle(socket, address):
        fp = socket.makefile()
        while True:

            try:
                line = fp.readline()
                line = str(line).rstrip()
            except:
                socket.send(b"Error in reading input.")
                socket.close()
                break

            if line == "QUIT":
                socket.close()
                break

            if line:
                lexer = redisbymmLexer(InputStream(line))
                stream = CommonTokenStream(lexer)
                parser = redisbymmParser(stream)

                parser.removeErrorListeners()
                parser.addErrorListener(RedisByMMErrorListener(socket))
                tree = parser.expression()
                listener = RedisByMMListener(socket)
                walker = ParseTreeWalker()
                walker.walk(listener, tree)
            else:
                continue

    server = StreamServer(("127.0.0.1", 1234), handle)
    server.serve_forever()


if __name__ == "__main__":
    run()

Całość jest dość prosta i polega na uruchomieniu serwera TCP używając gevent’a – więcej o tym możesz poczytać w tym wpisie.
Skupimy się tylko na tym kawałku:

1
2
3
4
5
6
7
                lexer = redisbymmLexer(InputStream(line))
                stream = CommonTokenStream(lexer)
                parser = redisbymmParser(stream)
                tree = parser.expression()
                listener = RedisByMMListener(socket)
                walker = ParseTreeWalker()
                walker.walk(listener, tree)

W nim, dla każdego polecenia które przyjdzie do serwera, tworzymy obiekt leksera, parsera i listenera. Działanie leksera i parsera jest dla nas nieco „przeźroczyste” – my implementacje dokonujemy w Listenerze. Listener pozwala nam wykonać odpowiednie czynności przy „wejściu” na token jak i przy „wyjściu” z tokena. Nasza klasa powinna dziedziczyć po klasie listenera która została dla nas wygenerowana a naszym zadaniem jest nadpisanie tylko odpowiednich funkcji:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class RedisByMMListener(redisbymmListener):
    def __init__(self, socket):
        self.socket = socket
        self.current_module = None

    def enterGet(self, ctx: redisbymmParser.GetContext):
        self.current_module = ModuleFactory.create_for_token("GET")

    def exitGet(self, ctx: redisbymmParser.GetContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterSetv(self, ctx: redisbymmParser.GetContext):
        self.current_module = ModuleFactory.create_for_token("SETV")

    def exitSetv(self, ctx: redisbymmParser.SetvContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterGettype(self, ctx: redisbymmParser.GettypeContext):
        self.current_module = ModuleFactory.create_for_token("GETTYPE")

    def exitGettype(self, ctx: redisbymmParser.GettypeContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterSadd(self, ctx: redisbymmParser.SaddContext):
        self.current_module = ModuleFactory.create_for_token("SADD")

    def exitSadd(self, ctx: redisbymmParser.SaddContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterSdel(self, ctx: redisbymmParser.SdelContext):
        self.current_module = ModuleFactory.create_for_token("SDEL")

    def exitSdel(self, ctx: redisbymmParser.SdelContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterSget(self, ctx: redisbymmParser.SgetContext):
        self.current_module = ModuleFactory.create_for_token("SGET")

    def exitSget(self, ctx: redisbymmParser.SgetContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterLadd(self, ctx: redisbymmParser.LaddContext):
        self.current_module = ModuleFactory.create_for_token("LADD")

    def exitLadd(self, ctx: redisbymmParser.LaddContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterLdel(self, ctx: redisbymmParser.LdelContext):
        self.current_module = ModuleFactory.create_for_token("LDEL")

    def exitLdel(self, ctx: redisbymmParser.LdelContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterLget(self, ctx: redisbymmParser.LgetContext):
        self.current_module = ModuleFactory.create_for_token("LGET")

    def exitLget(self, ctx: redisbymmParser.LgetContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterLgetall(self, ctx: redisbymmParser.LgetallContext):
        self.current_module = ModuleFactory.create_for_token("LGETALL")

    def exitLgetall(self, ctx: redisbymmParser.LgetallContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterLpop(self, ctx: redisbymmParser.LpopContext):
        self.current_module = ModuleFactory.create_for_token("LPOP")

    def exitLpop(self, ctx: redisbymmParser.LpopContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterDelk(self, ctx: redisbymmParser.DelkContext):
        self.current_module = ModuleFactory.create_for_token("DELK")

    def exitDelk(self, ctx: redisbymmParser.DelkContext):
        result = self.current_module.run()
        self.socket.send(result)

    def enterIndex(self, ctx: redisbymmParser.IndexContext):
        self.current_module.set_index(ctx.getText())

    def exitKey(self, ctx: redisbymmParser.KeyContext):
        self.current_module.set_key(ctx.getText())

    def exitVal(self, ctx: redisbymmParser.ValContext):
        self.current_module.set_value(
            ctx.getText(), create=isinstance(self.current_module, LaddModule)
        )

    def exitString_index(self, ctx: redisbymmParser.String_indexContext):
        self.current_module.set_string_index(ctx.getText())

Wszystkie przeciążone tu metody znajdują się w klasie nadrzędnej. Zobacz proszę na którąkolwiek parę funkcji „enter-exit” np:

1
2
3
4
5
6
    def enterGet(self, ctx: redisbymmParser.GetContext):
        self.current_module = ModuleFactory.create_for_token("GET")

    def exitGet(self, ctx: redisbymmParser.GetContext):
        result = self.current_module.run()
        self.socket.send(result)

Przy każdym wejściu w token (czyli de facto przy każdym poleceniu skierowanym do bazy danych) używamy fabryki modułów i zapisujemy stworzoną klasę jako „current_module” – gdyż w ramach jednego połączenia nie będziemy mieli więcej niż jednej komendy w jednym czasie. Zobaczmy jak wygląda fabryka:

1
2
3
4
5
6
7
8
9
10
import sys
import module_factory.modules


class ModuleFactory:
    @staticmethod
    def create_for_token(token):
        class_name = str(token[0]).upper() + str(token[1:]).lower() + "Module"
        print(class_name)
        return getattr(sys.modules[__name__ + ".modules"], class_name)()

Nie robi ona zbyt dużo – tworzy ona z przekazanego argumentu (nazwy tokena) nazwę modułu odpowiedzialnego za wykonanie logiki, wiec np. dla „GET” będzie to klasa „GetModule”. Plik ten będzie init’em modułu „modules”, czyli znajdzie się w katalogu modules, w pliki __init__.py:

Każdy moduł będzie działał na „contextach” jak i na obiekcie bazy danych. Każdy moduł będzie musiał dziedziczyć po klasie która pozwoli mu „żyć” w ekosystemie projektu. Klasą bazową dla każdego modułu będzie klasa Module:

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
32
33
34
class Module:
    def __init__(self):
        self.contexts = []
        self._initialize_definition()

    def run(self):
        assert len(self.contexts) > 0
        result = self._run()
        if result:
            return str.encode(result)
        else:
            return str.encode("")

    @abstractmethod
    def _run(self):
        raise NotImplementedError()

    def _initialize_definition(self):
        self.context = self.ClassContext

    def set_key(self, key):
        self.contexts.append(self.context(key=key))

    def set_value(self, value, create=False):
        if create:
            self.contexts.append(self.context(key=None, value=value))
            return
        self.contexts[-1].value = value

    def set_index(self, index):
        self.contexts[-1].index = index

    def set_string_index(self, string_index):
        self.contexts[-1].string_index = string_index

Jak pewnie się domyślasz po ciele tej klasy – każdy moduł będzie musiał mieć zdefiniowaną swoją metodę „_run” oraz posiadać klasę wewnętrzną o nazwie „ClassContext” – czyli naszym kontekście wywołania modułu.

Kontekst to nic innego jak lista argumentów dla modułu. Zauważ że poza metodami inicjalizującymi są jeszcze metody z serii „set_*” pozwalające zbudować pełny kontekst.

Przejdźmy przez te metody może na przykładzie polecenia:

1
 SET_KEY SPACE key SPACE val (SPACE key SPACE val)*

Czyli np:

1
 SETV aa bb

Przetwarzając takie polecenie wejdziemy pierw do metody „enterSetv” gdzie zbudujemy moduł odpowiedzialny za to polecenie. Moduł jest jeszcze bez kontekstu – tzn że nie zebraliśmy dla niego argumentów. Parser „przegląda” zadany mu string od lewej do prawej. Więc jak już pisałem – pierw trafi na słowo SETV i odpali dla niego funkcję enterSetV a My w niej stworzymy sobie odpowiednią klasę i zapamiętamy ją:)

Dalej w gramatyce możemy trafić na spację – olewamy i dalej na „klucz”. Nie mamy w parserze zdefiniowanej funkcji „enterKey” gdyż nie ma takiej potrzeby. Za to mamy „exitKey”:

1
2
    def exitKey(self, ctx: redisbymmParser.KeyContext):
        self.current_module.set_key(ctx.getText())

Zobacz że w nim w ramach zapamiętanego modułu odpalamy „set_key”, czyli:

1
2
    def set_key(self, key):
        self.contexts.append(self.context(key=key))

a więc tworzymy nowy kontekst (odpowiedni dla wybranego modułu) uzupełniając go o wartość wybranego klucza.

Ufff. Niby skomplikowane.. Ale nie do końca.

nie mogłem się powstrzymać :D

Dość śmieszków, lecimy dalej:)
Jak już dla modułu SETV zebraliśmy „klucz” czyli w naszym przykładzie „aa” to czas na zebranie wartości.
Parser odpali wtedy metodę exitVal a ona wykona metodą set_value uzupełniając nasz kontekst o wartość. I teraz na wyjściu całego polecenia, czyli na „exitSetv” możemy wykonać metodę „run” naszego modułu a jej rezultat wysłać do podłączonego klienta!

Zobacz że w przypadku komendy SETV dałem możliwość utworzenia kilku klucz na raz, np:

1
> SETV k1 v1 k2 v2 k3 v3

Ale nic to nie zmienia w gruncie rzeczy dla naszej implementacji – po prostu dla takiego polecenia zbierzemy trzy konteksty:)

Może żeby wszystko było turbo jasne i proste przejdziemy przez proces dla jeszcze jednego polecenia, np:

1
LADD_KEY SPACE key SPACE val (SPACE val)*

Czyli polecenia dodającego element do listy. A dokładnie wiele elementów do jednej listy. Dla przykładu:

1
 LADD k1 v1 v2 v3

Parser przeglądając dostarczony mu kod zacznie przeglądać go od lewej do prawej czyli pierwsze co znajdzie to słowo „LADD” i dopasuje go do gramatyki odpalając przy tym metodę „enterLadd”:

1
2
    def enterLadd(self, ctx: redisbymmParser.LaddContext):
        self.current_module = ModuleFactory.create_for_token("LADD")

która korzystając z fabryki stworzy i zapamięta odpowiedni moduł. Moduł który zostanie wybrany to LaddModule:

1
2
3
4
5
6
7
8
9
10
class LaddModule(Module):
    class ClassContext:
        def __init__(self, key=None, value=None):
            self.key = key
            self.value = value

    def _run(self):
        key = self.contexts[0].key
        print("ladd: {}".format(len(self.contexts)))
        [database.add_to_list(key, c.value) for c in self.contexts[1:]]

Dalej parser (pomijając spacje) trafi na „klucz” i zgodnie z wcześniejszym opisem – w metodzie „exitKey” stworzy kontekst dla wcześniej wybranego modułu i uzupełni go o wartość „key”.

Nie bez przyczyny wybrałem komendę LADD, gdyż różni się ona od reszty jedną rzeczą – ma ona różną ilość kluczy i wartości a dokładniej – ma jeden klucz i niezliczoną ilość wartości. Generuje nam to problem, gdyż dotychczas mieliśmy sytuacje jasną – tworzymy klucz, potem dopisujemy wartość, kolejny klucz i kolejna wartość i tak dalej. Każdy klucz to nowa instancja klasy ClassContext odpowiedniej dla danego modułu. Teraz mam zgrzyt pod tytułem – tworzymy klucz, potem dopisujemy wartość, potem znów wartość.. I tu jeb – nie mam kontekstu do którego moglibyśmy dopisać wartość (nie miało co nam go stworzyć). Problem rozwiązałem warunkowym „doklejaniem” nowego kontekstu w momencie kolekcjonowania wartości:

1
2
3
4
    def exitVal(self, ctx: redisbymmParser.ValContext):
        self.current_module.set_value(
            ctx.getText(), create=isinstance(self.current_module, LaddModule)
        )

a samo set_value rozszerzyłem to:

1
2
3
4
5
    def set_value(self, value, create=False):
        if create:
            self.contexts.append(self.context(key=None, value=value))
            return
        self.contexts[-1].value = value

co jednoznacznie rozwiązuje nam problem:)

W ramach praktyki zachęcam do zaproponowania lepszego rozwiązania tego problemu – gdyż trochę słabo robić warunek dla jednego modułu.

Doobra, skoro już wiemy jak działa listener i jak kolekcjonujemy w pamięci argumenty dla każdego polecenia – czas zajrzeć do implementacji faktycznej bazy danych.

Dobra, więc nasza baza danych będzie reprezentowana w pamięci jako słownik. Zaskakujące, prawda? ;)

A każdy moduł będzie sobie odpalał konkretną metodą naszego interfejsu, np dla LADD funkcja _run() będzie wyglądać:

1
2
3
4
5
6
7
8
9
10
class LaddModule(Module):
    class ClassContext:
        def __init__(self, key=None, value=None):
            self.key = key
            self.value = value

    def _run(self):
        key = self.contexts[0].key
        print("ladd: {}".format(len(self.contexts)))
        [database.add_to_list(key, c.value) for c in self.contexts[1:]]

Idąc tym tropem sprawdźmy więc jak wygląda moduł database.

Metoda „add_to_list”:

1
2
3
4
5
    def add_to_list(self, key, val):
        if key in self.__data:
            self.__data[key].append(val)
        else:
            self.__data[key] = [val]

jest bardzo prosta – jeśli pod kluczem nic nie ma, to co tworzymy, jeśli jest – to dopisujemy. Warto jednak zatrzymać się na chwilę nad tym czym jest „self.__data” :) ponieważ tu jest coś ciekawego:

1
__data = SynchronizedAccessDict()

Więc czym jest ten SynchronizedAccessDict? To coś co pomoże nam spełnić wymóg atomowości każdego polecenia.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SynchronizedAccessDict(dict):
    def __setitem__(self, k, v) -> None:
        named_semaphore.for_name(k).acquire()
        super().__setitem__(k, v)
        named_semaphore.for_name(k).release()

    def __getitem__(self, item):
        named_semaphore.for_name(item).acquire()
        try:
            return super().__getitem__(item)
        except KeyError:
            raise KeyError
        finally:
            named_semaphore.for_name(item).release()

    def __delitem__(self, v) -> None:
        named_semaphore.for_name(v).acquire()
        try:
            super().__delitem__(v)
        except KeyError:
            raise KeyError
        finally:
            named_semaphore.for_name(v).release()

Przeciążamy tutaj metody związane z operacją na słowniku i opakowujemy je w chroniący je nazwany semafor. Dzięki czemu dwie operacje na tym samym klucz zawsze wykonają się sekwencyjnie. Stan naszej bazy będzie spójny. Ten byt – nazwany semafor dopisałem sobie i wygląda tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class NamedSemaphore:
    __semaphores = {}

    def __init__(self):
        pass

    def for_name(self, name):
        if name in self.__semaphores:
            return self.__semaphores[name]
        else:
            semaphore = Semaphore(1)
            self.__semaphores[name] = semaphore
            return semaphore

    def release_all(self):
        [s.release() for s in self.__semaphores.values()]

czyli nic innego jak słownik (kolejny) semaforów – opuszczanych i zakładanych w razie potrzeby:)

Łał, mamy już sporo! Mamy gramatykę, parser, lekser, listener, obsługę atomowości… W sumie to można by już kończyć, ale została nam jeszcze jedna rzecz. Parser rzuca nam całkiem przyjemne błędy, warto je zwrócić użytkownikowi, np. podając samą komendę parser sugeruje że dalej powinna być spacja:

1
2
- LADD
- line 1:4 mismatched input '<EOF>' expecting SPACE

lub

1
2
3
LGET k1 r
line 1:8 mismatched input 'r' expecting DIGIT
Runtime_Err: Invalid index (expexting int)

Gdzie w drugim przypadku – pierwsza linijka jest od parsera a druga od nas – jako runtime error.

ewentualnie w przypadku słownika:

1
2
3
SGET k1 4
line 1:8 mismatched input '4' expecting LETTER
Runtime_Err: No such key!

gdzie chcieliśmy pobrać coś ze słownika korzystając przy tym z klucza „intowego”. Runtime od nas informujący że nie dość że składnia jest nie poprawna to jeszcze pytamy o klucz który nie istnieje!

Dobra, żeby dodać komunikaty parsera do komunikacji z klientem należy stworzyć nowy listener i dodać go:

1
2
3
4
5
6
7
8
9
10
                lexer = redisbymmLexer(InputStream(line))
                stream = CommonTokenStream(lexer)
                parser = redisbymmParser(stream)

                parser.removeErrorListeners()
                parser.addErrorListener(RedisByMMErrorListener(socket))
                tree = parser.expression()
                listener = RedisByMMListener(socket)
                walker = ParseTreeWalker()
                walker.walk(listener, tree)

mowa oczywiście o

1
2
                parser.removeErrorListeners()
                parser.addErrorListener(RedisByMMErrorListener(socket))

Natomiast sam ErrorListener jest bardzo prosty:

1
2
3
4
5
6
7
8
9
10
class RedisByMMErrorListener(ErrorListener):
    def __init__(self, socket):
        self.socket = socket
        self.not_important_msgs = ["no viable alternative at input '<EOF>'"]

    def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
        named_semaphore.release_all()
        error_msg = "line " + str(line) + ":" + str(column) + " " + msg
        if msg not in self.not_important_msgs:
            self.socket.send(str(error_msg).encode())

wyśle on wygenerowany, przefiltrowany błąd do użytkownika podłączonego do bazy.

Zerknij na demo naszego języka:

Powoli kończąc – nie wiem czy widzisz tutaj ogrom możliwość jakie ta technologia daje. Ja stworzyłem język deklaratywny, ale nic nie stoi na drodze byś Ty stworzył język imperatywny:)
Spokojnie jesteś w stanie po przeczytaniu tych dwóch wpisów stworzyć język który będzie albo dla zabawy – np będzie miał składnie wzorowaną na memach albo dla zysku – będzie np. obsługiwał kilka centrali o różnych interfejsach:)

Oczywiście kod który przedstawiłem nie jest wolny od błędów – np dodaj wartość string pod jakimś kluczem a potem potraktuj ten klucz metodą np. LADD :) exception gwarantowany:)

Tak czy siak – pokazuje ideę, więc jest wartościowy. Kod udostępniam zainteresowanym – https://gitlab.com/kajzur/redisbymm

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

0 komentarzy
Inline Feedbacks
View all comments