server.py 3.55 KB
Newer Older
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
# server.py - an asynchronous tcp-server to compile sources with DHParser
#
# Copyright 2019  by Eckhart Arnold (arnold@badw.de)
#                 Bavarian Academy of Sciences an Humanities (badw.de)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.  See the License for the specific language governing
# permissions and limitations under the License.


"""
Module `server` contains an asynchronous tcp-server that receives compilation
requests, runs custom compilation functions in a multiprocessing.Pool.

This allows to start a DHParser-compilation environment just once and save the
startup time of DHParser for each subsequent compilation. In particular, with
a just-in-time-compiler like PyPy (https://pypy.org) setting up a
compilation-server is highly recommended, because jit-compilers typically
sacrifice startup-speed for running-speed.

It is up to the compilation function to either return the result of the
compilation in serialized form, or just save the compilation results on the
file system an merely return an success or failure message. Module `server`
does not define any of these message. This is completely up to the clients
of module `server`, i.e. the compilation-modules, to decide.
"""


import asyncio
eckhart's avatar
eckhart committed
38
from multiprocessing import Value, Queue
39
40
41
42
from typing import Callable, Any

from DHParser.preprocess import BEGIN_TOKEN, END_TOKEN, TOKEN_DELIMITER
from DHParser.toolkit import get_config_value
43
44
45
46


# TODO: implement compilation-server!

47
48
49
50
SERVER_ERROR = "COMPILER-SERVER-ERROR"
CompileFunc = Callable[[str, str], Any]     # compiler_src(source: str, log_dir: str) -> Any


eckhart's avatar
eckhart committed
51
52
53
54
55
56
SERVER_OFFLINE    = 0
SERVER_STARTING   = 1
SERVER_ONLINE     = 2



57
58
59
60
class CompilerServer:
    def __init__(self, compiler: CompileFunc):
        self.compiler = compiler
        self.max_source_size = get_config_value('max_source_size')
eckhart's avatar
eckhart committed
61
62
        self.stage = Value('b', SERVER_OFFLINE)
        self.server_messages = Queue()
63

64
65
66
    async def handle_compilation_request(self,
                                   reader: asyncio.StreamReader,
                                   writer: asyncio.StreamWriter):
67
68
69
70
71
72
        data = await reader.read(self.max_source_size + 1)
        if len(data) > self.max_source_size:
            writer.write(BEGIN_TOKEN + SERVER_ERROR + TOKEN_DELIMITER +
                         "Source code to large! Only %iMB allowed." %
                         (self.max_source_size // (1024**2)) + END_TOKEN)
        else:
73
            writer.write(data)   # for now, only echo
74
75
        await writer.drain()
        writer.close()
76

di68kap's avatar
di68kap committed
77
78
    async def serve(self, address: str='127.0.0.1', port: int=8888):
        server = await asyncio.start_server(self.handle_compilation_request, address, port)
79
        async with server:
eckhart's avatar
eckhart committed
80
81
            self.stage.value = SERVER_ONLINE
            self.server_messages.put(SERVER_ONLINE)
di68kap's avatar
di68kap committed
82
            await server.serve_forever()
83

di68kap's avatar
di68kap committed
84
    def run_server(self, address: str='127.0.0.1', port: int=8888):
eckhart's avatar
eckhart committed
85
        self.stage.value = SERVER_STARTING
di68kap's avatar
di68kap committed
86
        asyncio.run(self.serve(address, port))
eckhart's avatar
eckhart committed
87
88
89
90
91
92

    def wait_until_server_online(self):
        if self.server_messages.get() != SERVER_ONLINE:
            raise AssertionError('could not start server!?')
        assert self.stage.value == SERVER_ONLINE