Merge ~ilasc/turnip:stats-collection-point into turnip:master

Proposed by Ioana Lasc
Status: Rejected
Rejected by: Ioana Lasc
Proposed branch: ~ilasc/turnip:stats-collection-point
Merge into: turnip:master
Diff against target: 108 lines (+62/-1)
2 files modified
turnip/pack/git.py (+40/-1)
turnip/pack/tests/test_functional.py (+22/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Needs Resubmitting
Review via email: mp+391365@code.launchpad.net

Commit message

Add metrics collection points to GitProcessProtocol

Description of the change

This MP uses print statements to show points in the code where we could potentially collect stats to be sent to statsd. The purpose of the MP is to give us a better starting point in discussing how and where is best to collect stats: in the PackBackendProtocol over pipes, inside the GitProcessProtocol itself, in a separate thread or otherwise.

So far I've tried several approaches:
1. wrap "default_reactor.spawnProcess(self.peer, cmd, args, env=env)" into a subprocess.call
2. write to GitProcessProtocol from the PackBackendProtocol and back
3. add additional fd to "default_reactor.spawnProcess(self.peer, cmd, args, env=env)" in the form "childFDs = { 0: "w", 1: "r", 2: "r" , 4: "w"}" and attempted to call writeToChild(4, _data_) but that didn't work as PackBackendProtocol is not a twisted.internet.protocol.ProcessProtocol
4. from the failure of option 3 I thought if I could introduce another layer of twisted.internet.protocol.ProcessProtocol between PBP and GitProcessProtocol I might be able to use additional pipes to preserve Git's normal output as well as communicate metrics.
5. spawn a "self written" separate thread to monitor the GitProcessProtocol process from outside
Essentially all 4 major approaches failed by breaking existing PBP <-> Git pipes or by not being able to pass metrics data through, I didn't pursue the 5th as I understand we do not want to go down the multithreading route.

The current proposal uses the GitProcessProtocol itself as a stats collection point.Running the "test_a_single_clone_and_push" unit test will produce:
turnip.pack.tests.test_functional.TestBackendFunctional.test_a_single_clone_and_push (v0 protocol) ...

PID 4545 rss with psutil: 0 MB
PID 4545 maxrss with getrusage: 88 MB
PID 4545 cpu utilization as a percentage 0.0
PID 4545 hostname: turnip-bionic
PID 4545 snapshot CPU times: pcputimes(user=0.0, system=0.0, children_user=0.0, children_system=0.0, iowait=0.0)
PID 4545 execution time: 0.856162071228
PID 4545 command: git-upload-pack
PID 4545 repository: /test

PID 4551 rss with psutil: 72 MB
PID 4551 maxrss with getrusage: 88 MB
PID 4551 cpu utilization as a percentage 0.0
PID 4551 hostname: turnip-bionic
PID 4551 snapshot CPU times: pcputimes(user=0.0, system=0.0, children_user=0.0, children_system=0.0, iowait=0.0)
PID 4551 execution time: 0.970222949982
PID 4551 command: git-receive-pack
PID 4551 repository: /test
ok

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

You were somewhat close with your option 3, but you had the extra file descriptor backwards. I'm expecting something more along the lines of this:

  https://paste.ubuntu.com/p/xB8TshVHj6/

Things to note:

 * the supervising process protocol arrangement remains essentially the same, with only the addition of handling an extra pipe for resource usage data;
 * we interpose an extra process between turnip-pack-backend and git, whose job is to collect resource usage information from the child and write it to a dedicated pipe
 * because we have an extra *process* (not a thread or something in-process in Twisted), we can reliably use getrusage(RUSAGE_CHILDREN) to collect *only* resource usage information about the git child process (and any descendant processes);
 * the data format for sending resource usage information back to turnip-pack-backend is private and can be whatever you like as long as it can be parsed reliably; I just used JSON since that was trivial to implement;
 * I had some trouble with the functional tests due to lots of UncleanReactorError exceptions; this looks like some subtle pre-existing race condition rather than something I've introduced, since it had a bad habit of going away when I tried to investigate using strace, so I just whacked it with AsynchronousDeferredRunTestForBrokenTwisted to spin the reactor another couple of times at the end of the test; this isn't ideal, but I wouldn't particularly mind it ending up on master;
 * I haven't implemented the statsd interaction, nor worked out how to label the metrics with information about the repository in question; that's important to do, but I stopped once I was satisfied that I had the process plumbing essentially right, and you should be able to take it from here.

review: Needs Resubmitting
Revision history for this message
Ioana Lasc (ilasc) wrote :

Marking this as rejected as Colin's patch and suggestions were incorporated into the larger branch which contains the "send to statsd/Telegraf" code, the formatting and tags for metrics, the singleton for the Statsd client and Mock and unit tests. (MP 391552)

Unmerged commits

af85d85... by Ioana Lasc

Add metrics collection points to GitProcessProtocol

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/turnip/pack/git.py b/turnip/pack/git.py
2index 4407085..f0d9244 100644
3--- a/turnip/pack/git.py
4+++ b/turnip/pack/git.py
5@@ -12,8 +12,11 @@ __metaclass__ = type
6
7
8 import uuid
9-
10+import psutil
11+import resource
12 import six
13+import socket
14+import time
15 from twisted.internet import (
16 defer,
17 error,
18@@ -242,14 +245,34 @@ class GitProcessProtocol(protocol.ProcessProtocol):
19 def __init__(self, peer):
20 self.peer = peer
21 self.out_started = False
22+ self.create_time = 0
23+ self.pid = 0
24+ self.os_process = None
25+ self.monitoring_thread = None
26+
27+ # increase number of processes counter here
28+ # and spwan send to statsd
29+ #self.statsd_client.incr('turnip.hostname.total.processes', rate=1)
30
31 def connectionMade(self):
32+ self.pid = self.transport.pid
33+ self.os_process = psutil.Process(pid = self.pid)
34+ self.create_time = self.os_process.create_time()
35+
36 self.peer.setPeer(self)
37 self.peer.transport.registerProducer(self, True)
38 self.transport.registerProducer(
39 UnstoppableProducerWrapper(self.peer.transport), True)
40 self.peer.resumeProducing()
41
42+ print('\n')
43+ print('PID ',self.pid, ' rss with psutil: ', self.os_process.memory_info().rss / 1024**2, ' MB')
44+ print('PID ',self.pid, ' maxrss with getrusage: ', resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024, ' MB')
45+ print('PID ',self.pid, ' cpu utilization as a percentage ', self.os_process.cpu_percent() / psutil.cpu_count())
46+ print('PID ',self.pid, ' hostname: ', socket.gethostname())
47+ print('PID ',self.pid, ' snapshot CPU times: ', self.os_process.cpu_times())
48+
49+
50 def outReceived(self, data):
51 self.out_started = True
52 self.peer.sendRawData(data)
53@@ -292,6 +315,22 @@ class GitProcessProtocol(protocol.ProcessProtocol):
54 self.transport.closeChildFD(0)
55
56 def processEnded(self, reason):
57+ # this instance of the GitProcessProtocol just ended
58+ # we record it duration for statsd metrics here
59+ process_duration = time.time() - self.create_time
60+ if (self.peer.command == b'git-upload-pack' or
61+ self.peer.command == b'git-receive-pack'):
62+ print('PID ',self.pid, ' execution time:',
63+ (time.time() - self.create_time))
64+ print('PID ',self.pid, ' command:', self.peer.command)
65+ print('PID ',self.pid, ' repository: ',self.peer.raw_pathname)
66+
67+ #self.statsd_client.timing('turnip.hostname.operation.timer', process_duration)
68+
69+ # decrease number of processes counter here
70+ # and spwan send to statsd
71+ #self.statsd_client.decr('turnip.hostname.total.processes')
72+
73 if reason.check(error.ProcessDone) and reason.value.exitCode != 0:
74 code = reason.value.exitCode
75 self.peer.log.info(
76diff --git a/turnip/pack/tests/test_functional.py b/turnip/pack/tests/test_functional.py
77index c06c4e2..34ea4f4 100644
78--- a/turnip/pack/tests/test_functional.py
79+++ b/turnip/pack/tests/test_functional.py
80@@ -179,6 +179,28 @@ class FunctionalTestMixin(WithScenarios):
81 defer.returnValue((out, err))
82
83 @defer.inlineCallbacks
84+ def test_a_single_clone_and_push(self):
85+ # Test a full clone, commit, push, clone, commit, push, pull
86+ # cycle using the backend server.
87+ test_root = self.useFixture(TempDir()).path
88+ clone1 = os.path.join(test_root, 'clone1')
89+
90+ # Clone the empty repo from the backend and commit to it.
91+ yield self.assertCommandSuccess((b'git', b'clone', self.url, clone1))
92+ yield self.assertCommandSuccess(
93+ (b'git', b'config', b'user.name', b'Test User'), path=clone1)
94+ yield self.assertCommandSuccess(
95+ (b'git', b'config', b'user.email', b'test@example.com'),
96+ path=clone1)
97+ yield self.assertCommandSuccess(
98+ (b'git', b'commit', b'--allow-empty', b'-m', b'Committed test'),
99+ path=clone1)
100+
101+ # Push it back up to the backend.
102+ yield self.assertCommandSuccess(
103+ (b'git', b'push', b'origin', b'master'), path=clone1)
104+
105+ @defer.inlineCallbacks
106 def test_clone_and_push(self):
107 # Test a full clone, commit, push, clone, commit, push, pull
108 # cycle using the backend server.

Subscribers

People subscribed via source and target branches