Skip to content

Commit a4daba4

Browse files
committed
Merge 'rebased'
1 parent 43e9c9c commit a4daba4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+933
-2164
lines changed

.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "isolate"]
2+
path = isolate
3+
url = https://github.com/cms-dev/isolate.git

AUTHORS.txt

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Kento Nikaido (aka snukent) <[email protected]>
2626
William Pettersson <[email protected]>
2727
Federico Scrinzi (aka volpino) <[email protected]>
2828
Luca Versari <[email protected]>
29+
Hyungsuk Yoon <[email protected]>
2930
Kenneth Wong <[email protected]>
3031

3132
And many other people that didn't write code, but provided useful

cms/__init__.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@
4141
# log
4242
# Nothing intended for external use, no need to advertise anything.
4343
# util
44-
"mkdir", "utf8_decoder", "Address", "ServiceCoord", "get_safe_shard",
45-
"get_service_address", "get_service_shards", "default_argument_parser",
44+
"ConfigError", "mkdir", "utf8_decoder", "Address", "ServiceCoord",
45+
"get_safe_shard", "get_service_address", "get_service_shards",
46+
"default_argument_parser",
4647
# conf
4748
"config",
4849
# plugin
4950
"plugin_list", "plugin_lookup",
50-
]
51+
]
5152

5253

5354
# Instantiate or import these objects.
@@ -102,7 +103,9 @@
102103
LANG_PASCAL: "lib.pas",
103104
}
104105

105-
from .util import mkdir, utf8_decoder, Address, ServiceCoord, get_safe_shard, \
106-
get_service_address, get_service_shards, default_argument_parser
106+
107+
from .util import ConfigError, mkdir, utf8_decoder, Address, ServiceCoord, \
108+
get_safe_shard, get_service_address, get_service_shards, \
109+
default_argument_parser
107110
from .conf import config
108111
from .plugin import plugin_list, plugin_lookup

cms/db/__init__.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
# drop
7575
"drop_db",
7676
# util
77-
"get_contest_list", "is_contest_id", "ask_for_contest",
77+
"test_db_connection", "get_contest_list", "is_contest_id",
78+
"ask_for_contest",
7879
# test
7980
"Test", "TestQuestion", "QuestionFile", "TestScore",
8081
# forum
@@ -84,7 +85,7 @@
8485

8586
# Instantiate or import these objects.
8687

87-
version = 12
88+
version = 13
8889

8990

9091
engine = create_engine(config.database, echo=config.database_debug,
@@ -113,7 +114,8 @@
113114
from .init import init_db
114115
from .drop import drop_db
115116

116-
from .util import get_contest_list, is_contest_id, ask_for_contest
117+
from .util import test_db_connection, get_contest_list, is_contest_id, \
118+
ask_for_contest
117119

118120

119121
configure_mappers()

cms/db/filecacher.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ def __init__(self, service=None, path=None, null=False, enabled=None):
436436
logger.error("Cannot create necessary directories.")
437437
raise RuntimeError("Cannot create necessary directories.")
438438

439-
def load(self, digest):
439+
def load(self, digest, if_needed=False):
440440
"""Load the file with the given digest into the cache.
441441
442442
Ask the backend to provide the file and, if it's available,
@@ -445,15 +445,20 @@ def load(self, digest):
445445
If caching is disabled, this function does nothing.
446446
447447
digest (unicode): the digest of the file to load.
448+
if_needed (bool): only load the file if it is not present in
449+
the local cache.
448450
449451
raise (KeyError): if the backend cannot find the file.
450452
451453
"""
452454
if self.enabled:
455+
cache_file_path = os.path.join(self.file_dir, digest)
456+
if if_needed and os.path.exists(cache_file_path):
457+
return
458+
453459
ftmp_handle, temp_file_path = tempfile.mkstemp(dir=self.temp_dir,
454460
text=False)
455461
ftmp = os.fdopen(ftmp_handle, 'w')
456-
cache_file_path = os.path.join(self.file_dir, digest)
457462

458463
fobj = self.backend.get_file(digest)
459464

cms/db/submission.py

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# Copyright © 2010-2012 Matteo Boscariol <[email protected]>
88
# Copyright © 2012-2013 Luca Wehrstedt <[email protected]>
99
# Copyright © 2013 Bernard Blackham <[email protected]>
10+
# Copyright © 2014 Fabian Gundlach <[email protected]>
1011
#
1112
# This program is free software: you can redistribute it and/or modify
1213
# it under the terms of the GNU Affero General Public License as
@@ -87,6 +88,17 @@ class Submission(Base):
8788
String,
8889
nullable=True)
8990

91+
# Comment from the administrator on the submission.
92+
comment = Column(
93+
Unicode,
94+
nullable=False,
95+
default="")
96+
97+
@property
98+
def short_comment(self):
99+
"""The first line of the comment."""
100+
return self.comment.split("\n", 1)[0]
101+
90102
# Follows the description of the fields automatically added by
91103
# SQLAlchemy.
92104
# files (dict of File objects indexed by filename)

cms/db/task.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# -*- coding: utf-8 -*-
33

44
# Contest Management System - http://cms-dev.github.io/
5-
# Copyright © 2010-2013 Giovanni Mascellani <[email protected]>
5+
# Copyright © 2010-2014 Giovanni Mascellani <[email protected]>
66
# Copyright © 2010-2012 Stefano Maggiolo <[email protected]>
77
# Copyright © 2010-2012 Matteo Boscariol <[email protected]>
88
# Copyright © 2012-2014 Luca Wehrstedt <[email protected]>
@@ -433,6 +433,16 @@ class Dataset(Base):
433433
# managers (dict of Manager objects indexed by filename)
434434
# testcases (dict of Testcase objects indexed by codename)
435435

436+
@property
437+
def active(self):
438+
"""Shorthand for detecting if the dataset is active.
439+
440+
return (bool): True if this dataset is the active one for its
441+
task.
442+
443+
"""
444+
return self is self.task.active_dataset
445+
436446

437447
class Manager(Base):
438448
"""Class to store additional files needed to compile or evaluate a

cms/db/util.py

+20
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,29 @@
3030

3131
import sys
3232

33+
from sqlalchemy.exc import OperationalError
34+
35+
from cms import ConfigError
3336
from . import SessionGen, Contest
3437

3538

39+
def test_db_connection():
40+
"""Perform an operation that raises if the DB is not reachable.
41+
42+
raise (sqlalchemy.exc.OperationalError): if the DB cannot be
43+
accessed (usually for permission problems).
44+
45+
"""
46+
try:
47+
# We do not care of the specific query executed here, we just
48+
# use it to ensure that the DB is accessible.
49+
with SessionGen() as session:
50+
session.execute("select 0;")
51+
except OperationalError:
52+
raise ConfigError("Operational error while talking to the DB. "
53+
"Is the connection string in cms.conf correct?")
54+
55+
3656
def get_contest_list(session=None):
3757
"""Return all the contest objects available on the database.
3858

cms/io/rpc.py

+47-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import gevent.socket
3838
import gevent.event
3939

40-
from cms import get_service_address
40+
from cms import Address, get_service_address
4141

4242

4343
logger = logging.getLogger(__name__)
@@ -418,6 +418,9 @@ def __init__(self, remote_service_coord, auto_retry=None):
418418
remote service in case the connection is lost; if not given
419419
no automatic reconnection attempts will occur.
420420
421+
raise (KeyError): if the coordinates are not specified in the
422+
configuration.
423+
421424
"""
422425
super(RemoteServiceClient, self).__init__(
423426
get_service_address(remote_service_coord))
@@ -651,3 +654,46 @@ def remote_method(**data):
651654
return result
652655

653656
return remote_method
657+
658+
659+
class FakeRemoteServiceClient(RemoteServiceClient):
660+
"""A RemoteServiceClient not actually connected to anything.
661+
662+
This is useful for connections to optional services, to avoid
663+
having to specify different behaviors in the case the services are
664+
or are not available.
665+
666+
In all aspects, an object of this class behaves exactly like a
667+
RemoteServiceClient that will never be able to connect.
668+
669+
"""
670+
def __init__(self, remote_service_coord, auto_retry=None):
671+
"""Initialization.
672+
673+
This constructor does not call the parent constructor, because
674+
it would fail (as the service coord are not in the
675+
configuration). This is potentially a problem, but as this
676+
client will never connect not many member variable access are
677+
performed.
678+
679+
"""
680+
RemoteServiceBase.__init__(self, Address("None", 0))
681+
self.remote_service_coord = remote_service_coord
682+
self.pending_outgoing_requests = dict()
683+
self.pending_outgoing_requests_results = dict()
684+
self.auto_retry = auto_retry
685+
686+
def connect(self):
687+
"""Do nothing, as this is a fake client."""
688+
pass
689+
690+
def disconnect(self):
691+
"""Do nothing, as this is a fake client."""
692+
pass
693+
694+
def execute_rpc(self, method, data):
695+
"""Just return an AsyncResult encoding an error."""
696+
result = gevent.event.AsyncResult()
697+
result.set_exception(
698+
RPCError("Called a method of a non-configured service."))
699+
return result

cms/io/service.py

+25-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# Contest Management System - http://cms-dev.github.io/
55
# Copyright © 2010-2014 Giovanni Mascellani <[email protected]>
6-
# Copyright © 2010-2013 Stefano Maggiolo <[email protected]>
6+
# Copyright © 2010-2014 Stefano Maggiolo <[email protected]>
77
# Copyright © 2010-2012 Matteo Boscariol <[email protected]>
88
# Copyright © 2013 Luca Wehrstedt <[email protected]>
99
#
@@ -37,7 +37,6 @@
3737
import signal
3838
import socket
3939
import _socket
40-
import sys
4140
import time
4241

4342
import gevent
@@ -46,12 +45,14 @@
4645
from gevent.server import StreamServer
4746
from gevent.backdoor import BackdoorServer
4847

49-
from cms import config, mkdir, ServiceCoord, Address, get_service_address
48+
from cms import ConfigError, config, mkdir, ServiceCoord, Address, \
49+
get_service_address
5050
from cms.log import root_logger, shell_handler, ServiceFilter, \
5151
CustomFormatter, LogServiceHandler, FileHandler
5252
from cmscommon.datetime import monotonic_time
5353

54-
from .rpc import rpc_method, RemoteServiceServer, RemoteServiceClient
54+
from .rpc import rpc_method, RemoteServiceServer, RemoteServiceClient, \
55+
FakeRemoteServiceClient
5556

5657

5758
logger = logging.getLogger(__name__)
@@ -99,9 +100,9 @@ def __init__(self, shard=0):
99100
try:
100101
address = get_service_address(self._my_coord)
101102
except KeyError:
102-
logger.critical("Couldn't find %r in the configuration.",
103-
self._my_coord)
104-
sys.exit(1)
103+
raise ConfigError("Unable to find address for service %r. "
104+
"Is it specified in core_services in cms.conf?" %
105+
(self._my_coord,))
105106

106107
self.rpc_server = StreamServer(address, self._connection_handler)
107108
self.backdoor = None
@@ -172,7 +173,8 @@ def _connection_handler(self, sock, address):
172173
remote_service = RemoteServiceServer(self, address)
173174
remote_service.handle(sock)
174175

175-
def connect_to(self, coord, on_connect=None, on_disconnect=None):
176+
def connect_to(self, coord, on_connect=None, on_disconnect=None,
177+
must_be_present=True):
176178
"""Return a proxy to a remote service.
177179
178180
Obtain a communication channel to the remote service at the
@@ -184,12 +186,26 @@ def connect_to(self, coord, on_connect=None, on_disconnect=None):
184186
connects.
185187
on_disconnect (function|None): to be called when it
186188
disconnects.
189+
must_be_present (bool): if True, the coord must be present in
190+
the configuration; otherwise, it can be missing and in
191+
that case the return value is a fake client (that is, a
192+
client that never connects and ignores all calls).
187193
188194
return (RemoteServiceClient): a proxy to that service.
189195
190196
"""
191197
if coord not in self.remote_services:
192-
service = RemoteServiceClient(coord, auto_retry=0.5)
198+
try:
199+
service = RemoteServiceClient(coord, auto_retry=0.5)
200+
except KeyError:
201+
# The coordinates are invalid: raise a ConfigError if
202+
# the service was needed, or return a dummy client if
203+
# the service was optional.
204+
if must_be_present:
205+
raise ConfigError("Missing address and port for %s "
206+
"in cms.conf." % (coord, ))
207+
else:
208+
service = FakeRemoteServiceClient(coord, None)
193209
service.connect()
194210
self.remote_services[coord] = service
195211
else:

0 commit comments

Comments
 (0)