Merge lp:~free.ekanayaka/landscape-client/socket-locks-fix into lp:~landscape/landscape-client/trunk

Proposed by Free Ekanayaka
Status: Merged
Approved by: Gavin Panella
Approved revision: 287
Merge reported by: Free Ekanayaka
Merged at revision: not available
Proposed branch: lp:~free.ekanayaka/landscape-client/socket-locks-fix
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 195 lines (+47/-13)
10 files modified
debian/changelog (+6/-0)
debian/landscape-client.init (+10/-0)
landscape/broker/tests/test_service.py (+1/-0)
landscape/manager/tests/test_service.py (+2/-0)
landscape/monitor/tests/test_service.py (+3/-0)
landscape/reactor.py (+1/-1)
landscape/service.py (+2/-1)
landscape/tests/helpers.py (+14/-10)
landscape/tests/test_reactor.py (+7/-1)
landscape/tests/test_service.py (+1/-0)
To merge this branch: bzr merge lp:~free.ekanayaka/landscape-client/socket-locks-fix
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Björn Tillenius (community) Approve
Review via email: mp+32314@code.launchpad.net

Description of the change

This branch fixes the client shutdown sequence, given Twisted the chance of unlinking sockets files and symlinks.

To post a comment you must log in.
284. By Free Ekanayaka

Cleanup sockets in the init-script in case it's needed

285. By Free Ekanayaka

Use tabs for consistency

286. By Free Ekanayaka

Typo

Revision history for this message
Björn Tillenius (bjornt) wrote :

< BjornT> free: can you expand a bit on what effect removing.
          self.port.stopListening() does? is that what fixes the issue,.
          or is it lines 93-94, and the rest are just cosmetic changes?
< free> BjornT: two things fix the issues, removing.
        self.port.stopListening() and replacing reactor.crash() with.
        reactor.stop()
< free> BjornT: this makes the shutdown sequence compliant with what.
        Twisted requires
< free> BjornT: and cleans up the sockets
< BjornT> free: ok, sounds good. what about the addition to the.
          changelog? shouldn't it list what got fixed?
< free> BjornT: I just added it to bump the version of the builds in the.
        PPA, we will finalize it when we do the actual release with.
        everything that was changed
< BjornT> free: ok, i trust you :)
< BjornT> +1

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Nice, a few comments. :)

[1]

+ if ls $SOCKETDIR/* > /dev/null 2>&1; then
+ if ! pidofproc -p $PIDFILE $DAEMON > /dev/null; then
+ rm -f $SOCKETDIR/*
+ fi
+ fi

OMGMYEYES... I just looked at the definition of pidofproc in
/lib/lsb/init-functions and found at least one bug just from reading
it.

Back to the snippet. If SOCKETDIR was ever not set this would expand
to:

    if ls /* > /dev/null 2>&1; then
        if ! pidofproc -p $PIDFILE $DAEMON > /dev/null; then
            rm -f /*
        fi
    fi

Which could have quite a detrimental effect on a system! It may be
that set -u is in force, but it's probably better to make sure it
breaks anyway:

    if ls ${SOCKETDIR:?}/* > /dev/null 2>&1; then
        if ! pidofproc -p $PIDFILE $DAEMON > /dev/null; then
            rm -f ${SOCKETDIR:?}/*
        fi
    fi

Even then, if SOCKETDIR had a trailing space (don't know why, but it
pays to be defensive), this could expand to:

    if ls /socket/dir /* > /dev/null 2>&1; then
        if ! pidofproc -p $PIDFILE $DAEMON > /dev/null; then
            rm -f /socket/dir /*
        fi
    fi

So I think it's best to find a way to avoid globbing altogether here,
and quote arguments:

    if ! pidofproc -p "$PIDFILE" "$DAEMON" > /dev/null
    then
        find "${SOCKETDIR:?}" -print0 | xargs -r0 rm -f
    fi

Also, when using rm -f, I'd feel more comfortable if the focus was
narrowed to only delete sockets, to minimize collateral damage if
things go wrong:

    if ! pidofproc -p "$PIDFILE" "$DAEMON" > /dev/null
    then
        find "${SOCKETDIR:?}" -type s -print0 | xargs -r0 rm -f
    fi

Sorry for tearing into this code. Scripting with bash and its ilk is
so difficult to make safe. Init scripts should be written in Python :)

[2]

     def get_reactor(self):
- return TwistedReactor()
+ reactor = TwistedReactor()
+ # It's not possible to stop the reactor in a Trial test, calling
+ # reactor.crash instead
+ reactor._reactor.stop = reactor._reactor.crash

The last line above modifies the global reactor. That doesn't seem
like a good thing to do.

I've also been told by Jono Lange that stopping and starting the
reactor is not supported. He said something along the lines of: it
might work for you, but it's known to be problematic, it's not
supported, and you get to keep the pieces when it goes wrong.

In short, reactor.stop() and reactor.crash() should not be used except
as part of process tear-down.

This means that the whole of TwistedReactorTest is problematic because
it calls these methods on the real reactor. It works though, so what
do I know? :)

review: Needs Fixing
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

[1]

Wow, that was a nice analysis. Thanks for all the hints it was quite interesting, changed as you suggest and also added:

   find "${SOCKETDIR:?}" -type l -print0 | xargs -r0 rm -f

because we need to delete the symbolic links that Twisted uses to keep track of the PID that created the socket.

[2]

Yeah I know, the whole TwistedReactorTest is somehow unfortunate (actually I've been wondering if we need a TwistedReactor class at all and if we could in principle go with the straight Twisted reactor). Those tests have been there for a long time and they seem to work (read "pass"), so I don't think we need to touch them for now, or at least not in this branch.

As for patching the global reactor, good catch :) I've added an addCleanup:

        saved_stop = reactor._reactor.stop
        reactor._reactor.stop = reactor._reactor.crash
        self.addCleanup(lambda: setattr(reactor._reactor, "stop", saved_stop))
        return reactor

which I believe fixes the issue.

287. By Free Ekanayaka

Address Gavin's comments

288. By Free Ekanayaka

OMG, I'm tired

Revision history for this message
Gavin Panella (allenap) wrote :

Cool, +1 :)

[2]

Thanks for the explanation, and I agree they should be left alone for now.

> self.addCleanup(lambda: setattr(reactor._reactor, "stop", saved_stop))

Not that it matters a lot, but I think this can be simplified to:

  self.addCleanup(setattr, reactor._reactor, "stop", saved_stop)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/changelog'
2--- debian/changelog 2010-07-28 06:30:31 +0000
3+++ debian/changelog 2010-08-12 15:59:39 +0000
4@@ -1,3 +1,9 @@
5+landscape-client (1.5.5-0ubuntu0.10.10.0) UNRELEASED; urgency=low
6+
7+ * New upstream version
8+
9+ -- Free Ekanayaka <free.ekanayaka@canonical.com> Wed, 11 Aug 2010 12:41:16 +0200
10+
11 landscape-client (1.5.4-0ubuntu0.10.10.0) maverick; urgency=low
12
13 * New upstream version (LP: #610744):
14
15=== modified file 'debian/landscape-client.init'
16--- debian/landscape-client.init 2010-05-11 08:02:29 +0000
17+++ debian/landscape-client.init 2010-08-12 15:59:39 +0000
18@@ -15,6 +15,7 @@
19 NAME=landscape-client
20 DAEMON=/usr/bin/$NAME
21 PIDDIR=/var/run/landscape
22+SOCKETDIR=/var/lib/landscape/client/sockets
23 PIDFILE=$PIDDIR/$NAME.pid
24 RUN=0 # overridden in /etc/default/landscape-client
25 CLOUD=0 # overridden in /etc/default/landscape-client
26@@ -58,6 +59,15 @@
27 mkdir $PIDDIR
28 chown landscape $PIDDIR
29 fi
30+ # Cleanup leftover sockets if there's no other landscape process
31+ # running. This shouldn't be usually necessary, but it can
32+ # happen in case the client crashed badly and the socket points
33+ # to a process with the same PID.
34+ if ! pidofproc -p "$PIDFILE" "$DAEMON" > /dev/null; then
35+ find "${SOCKETDIR:?}" -type s -print0 | xargs -r0 rm -f
36+ find "${SOCKETDIR:?}" -type l -print0 | xargs -r0 rm -f
37+ fi
38+
39 FULL_COMMAND="start-stop-daemon --start --quiet --oknodo --startas $DAEMON --pidfile $PIDFILE -g $DAEMON_GROUP -- --daemon --pid-file $PIDFILE"
40 if [ x"$DAEMON_USER" != x ]; then
41 sudo -u $DAEMON_USER $FULL_COMMAND
42
43=== modified file 'landscape/broker/tests/test_service.py'
44--- landscape/broker/tests/test_service.py 2010-04-23 14:42:21 +0000
45+++ landscape/broker/tests/test_service.py 2010-08-12 15:59:39 +0000
46@@ -94,4 +94,5 @@
47 connected.addCallback(lambda remote: remote.get_server_uuid())
48 connected.addCallback(lambda x: connector.disconnect())
49 connected.addCallback(lambda x: self.service.stopService())
50+ connected.addCallback(lambda x: self.service.port.stopListening())
51 return connected
52
53=== modified file 'landscape/manager/tests/test_service.py'
54--- landscape/manager/tests/test_service.py 2010-04-19 13:24:13 +0000
55+++ landscape/manager/tests/test_service.py 2010-08-12 15:59:39 +0000
56@@ -54,7 +54,9 @@
57 [connector] = self.broker_service.broker.get_connectors()
58 connector.disconnect()
59 self.service.stopService()
60+ self.service.port.stopListening()
61 self.broker_service.stopService()
62+ self.broker_service.port.stopListening()
63
64 def assert_broker_connection(ignored):
65 self.assertEquals(len(self.broker_service.broker.get_clients()), 1)
66
67=== modified file 'landscape/monitor/tests/test_service.py'
68--- landscape/monitor/tests/test_service.py 2010-04-19 13:24:13 +0000
69+++ landscape/monitor/tests/test_service.py 2010-08-12 15:59:39 +0000
70@@ -57,7 +57,9 @@
71 [connector] = self.broker_service.broker.get_connectors()
72 connector.disconnect()
73 self.service.stopService()
74+ self.service.port.stopListening()
75 self.broker_service.stopService()
76+ self.broker_service.port.stopListening()
77
78 def assert_broker_connection(ignored):
79 self.assertEquals(len(self.broker_service.broker.get_clients()), 1)
80@@ -82,3 +84,4 @@
81 self.service.port.stopListening()
82 self.mocker.replay()
83 self.service.stopService()
84+ self.service.port.stopListening()
85
86=== modified file 'landscape/reactor.py'
87--- landscape/reactor.py 2010-04-23 14:42:21 +0000
88+++ landscape/reactor.py 2010-08-12 15:59:39 +0000
89@@ -379,7 +379,7 @@
90 def stop(self):
91 """Stop the reactor, a C{"stop"} event will be fired."""
92
93- self._reactor.crash()
94+ self._reactor.stop()
95 self._cleanup()
96
97 def time(self):
98
99=== modified file 'landscape/service.py'
100--- landscape/service.py 2010-05-03 13:08:02 +0000
101+++ landscape/service.py 2010-08-12 15:59:39 +0000
102@@ -45,7 +45,8 @@
103 self.port = self.reactor.listen_unix(self.socket, self.factory)
104
105 def stopService(self):
106- self.port.stopListening()
107+ # We don't need to call port.stopListening(), because the reactor
108+ # shutdown sequence will do that for us.
109 Service.stopService(self)
110 logging.info("%s stopped with config %s" % (
111 self.service_name.capitalize(), self.config.get_config_filename()))
112
113=== modified file 'landscape/tests/helpers.py'
114--- landscape/tests/helpers.py 2010-07-30 13:05:59 +0000
115+++ landscape/tests/helpers.py 2010-08-12 15:59:39 +0000
116@@ -26,13 +26,9 @@
117 from landscape.monitor.monitor import Monitor
118 from landscape.manager.manager import Manager
119
120-# FIXME: We can drop the "_" suffix and replace the current classes once the
121-# AMP migration is completed
122-from landscape.broker.service import BrokerService as BrokerService_
123-from landscape.broker.amp import (
124- FakeRemoteBroker as FakeRemoteBroker_, RemoteBrokerConnector)
125-from landscape.manager.config import (
126- ManagerConfiguration as ManagerConfiguration_)
127+from landscape.broker.service import BrokerService
128+from landscape.broker.amp import FakeRemoteBroker, RemoteBrokerConnector
129+from landscape.manager.config import ManagerConfiguration
130
131
132 DEFAULT_ACCEPTED_TYPES = [
133@@ -284,12 +280,20 @@
134 config = BrokerConfiguration()
135 config.load(["-c", test_case.config_filename])
136
137- class FakeBrokerService(BrokerService_):
138+ class FakeBrokerService(BrokerService):
139 reactor_factory = FakeReactor
140 transport_factory = FakeTransport
141
142+ def stopService(service):
143+ # We need to explictely stop listening to the socket
144+ # because the reactor would still have active selectables
145+ # at the end of the test otherwise
146+ if os.path.exists(service.port.port):
147+ service.port.connectionLost(None)
148+ super(FakeBrokerService, service).stopService()
149+
150 test_case.broker_service = FakeBrokerService(config)
151- test_case.remote = FakeRemoteBroker_(
152+ test_case.remote = FakeRemoteBroker(
153 test_case.broker_service.exchanger,
154 test_case.broker_service.message_store)
155
156@@ -351,7 +355,7 @@
157
158 def set_up(self, test_case):
159 super(ManagerHelper, self).set_up(test_case)
160- test_case.config = ManagerConfiguration_()
161+ test_case.config = ManagerConfiguration()
162 test_case.config.load(["-c", test_case.config_filename])
163 test_case.reactor = FakeReactor()
164 test_case.manager = Manager(test_case.reactor, test_case.config)
165
166=== modified file 'landscape/tests/test_reactor.py'
167--- landscape/tests/test_reactor.py 2010-04-23 14:42:21 +0000
168+++ landscape/tests/test_reactor.py 2010-08-12 15:59:39 +0000
169@@ -395,7 +395,13 @@
170 class TwistedReactorTest(LandscapeTest, ReactorTestMixin):
171
172 def get_reactor(self):
173- return TwistedReactor()
174+ reactor = TwistedReactor()
175+ # It's not possible to stop the reactor in a Trial test, calling
176+ # reactor.crash instead
177+ saved_stop = reactor._reactor.stop
178+ reactor._reactor.stop = reactor._reactor.crash
179+ self.addCleanup(lambda: setattr(reactor._reactor, "stop", saved_stop))
180+ return reactor
181
182 def test_real_time(self):
183 reactor = self.get_reactor()
184
185=== modified file 'landscape/tests/test_service.py'
186--- landscape/tests/test_service.py 2010-05-03 13:08:02 +0000
187+++ landscape/tests/test_service.py 2010-08-12 15:59:39 +0000
188@@ -114,6 +114,7 @@
189 self.assertTrue(service.port.connected)
190 connector.disconnect()
191 service.stopService()
192+ return service.port.stopListening()
193
194 connected = connector.connect()
195 return connected.addCallback(assert_port)

Subscribers

People subscribed via source and target branches

to all changes: