Merge ~pappacena/turnip:repo-async-creation-status into turnip:master

Proposed by Thiago F. Pappacena
Status: Work in progress
Proposed branch: ~pappacena/turnip:repo-async-creation-status
Merge into: turnip:master
Diff against target: 198 lines (+72/-15)
4 files modified
config.yaml (+1/-0)
turnip/api/store.py (+67/-13)
turnip/api/views.py (+2/-1)
turnip/tasks.py (+2/-1)
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+400493@code.launchpad.net

Commit message

Reporting status on repositories that are currently being created

To post a comment you must log in.

Unmerged commits

5f18244... by Thiago F. Pappacena

Reporting status on repositories that are currently being created

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/config.yaml b/config.yaml
2index 0273840..e216a3a 100644
3--- a/config.yaml
4+++ b/config.yaml
5@@ -21,6 +21,7 @@ openid_provider_root: https://testopenid.test/
6 site_name: git.launchpad.test
7 main_site_root: https://launchpad.test/
8 celery_broker: pyamqp://guest@localhost//
9+celery_backend: file:///var/tmp/git.launchpad.test
10 statsd_host: 127.0.0.1
11 statsd_port: 8125
12 statsd_prefix: turnip
13diff --git a/turnip/api/store.py b/turnip/api/store.py
14index f2719a7..ca3b194 100644
15--- a/turnip/api/store.py
16+++ b/turnip/api/store.py
17@@ -3,6 +3,7 @@
18
19 import base64
20 import itertools
21+import json
22 import logging
23 import os
24 import re
25@@ -295,7 +296,8 @@ class AlreadyExistsError(GitError):
26
27
28 def init_repo(repo_path, clone_from=None, clone_refs=False,
29- alternate_repo_paths=None, is_bare=True, log=None):
30+ alternate_repo_paths=None, is_bare=True, log=None,
31+ metadata=None):
32 """Initialise a new git repository or clone from existing."""
33 if os.path.exists(repo_path):
34 raise AlreadyExistsError(repo_path)
35@@ -305,7 +307,7 @@ def init_repo(repo_path, clone_from=None, clone_refs=False,
36 init_repository(repo_path, is_bare)
37
38 log.info("Running set_repository_creating(%s, True)" % repo_path)
39- set_repository_creating(repo_path, True)
40+ set_repository_creating(repo_path, True, metadata)
41
42 if clone_from:
43 # The clone_from's objects and refs are in fact cloned into a
44@@ -354,31 +356,41 @@ def init_repo(repo_path, clone_from=None, clone_refs=False,
45 ensure_config(repo_path) # set repository configuration defaults
46
47 log.info("Running set_repository_creating(%s, False)" % repo_path)
48+ # XXX: pappacena: Remove this. It's here just to pretend a fork takes
49+ # time while testing it locally.
50+ import time; time.sleep(30)
51 set_repository_creating(repo_path, False)
52
53
54-@app.task
55-def init_and_confirm_repo(untranslated_path, repo_path, clone_from=None,
56+@app.task(bind=True)
57+def init_and_confirm_repo(self, untranslated_path, repo_path, clone_from=None,
58 clone_refs=False, alternate_repo_paths=None,
59 is_bare=True):
60- logger = tasks_logger
61- xmlrpc_endpoint = config.get("virtinfo_endpoint")
62- xmlrpc_timeout = float(config.get("virtinfo_timeout"))
63- xmlrpc_auth_params = {"user": "+launchpad-services"}
64- xmlrpc_proxy = TimeoutServerProxy(
65- xmlrpc_endpoint, timeout=xmlrpc_timeout, allow_none=True)
66+ self.update_state(state='RUNNING')
67+ try:
68+ logger = tasks_logger
69+ xmlrpc_endpoint = config.get("virtinfo_endpoint")
70+ xmlrpc_timeout = float(config.get("virtinfo_timeout"))
71+ xmlrpc_auth_params = {"user": "+launchpad-services"}
72+ xmlrpc_proxy = TimeoutServerProxy(
73+ xmlrpc_endpoint, timeout=xmlrpc_timeout, allow_none=True)
74+ except:
75+ self.update_state(state="FAILED")
76+ raise
77 try:
78 logger.info(
79 "Initializing and confirming repository creation: "
80 "%s; %s; %s; %s; %s", repo_path, clone_from, clone_refs,
81 alternate_repo_paths, is_bare)
82 init_repo(
83- repo_path, clone_from, clone_refs, alternate_repo_paths, is_bare)
84+ repo_path, clone_from, clone_refs, alternate_repo_paths, is_bare,
85+ metadata={"task_id": self.request.id})
86 logger.debug(
87 "Confirming repository creation: %s; %s",
88 untranslated_path, xmlrpc_auth_params)
89 xmlrpc_proxy.confirmRepoCreation(untranslated_path, xmlrpc_auth_params)
90 except Exception as e:
91+ self.update_state(state='FAILED')
92 logger.error("Error creating repository at %s: %s", repo_path, e)
93 try:
94 delete_repo(repo_path)
95@@ -388,6 +400,8 @@ def init_and_confirm_repo(untranslated_path, repo_path, clone_from=None,
96 "Aborting repository creation: %s; %s",
97 untranslated_path, xmlrpc_auth_params)
98 xmlrpc_proxy.abortRepoCreation(untranslated_path, xmlrpc_auth_params)
99+ finally:
100+ self.update_state(state='SUCCESS')
101
102
103 @contextmanager
104@@ -435,17 +449,26 @@ def get_default_branch(repo_path):
105 return repo.references['HEAD'].target
106
107
108-def set_repository_creating(repo_path, is_creating):
109+def set_repository_creating(repo_path, is_creating, metadata=None):
110+ """Set the repository state as "creating" or "not creating".
111+
112+ If is_creating is True, metadata will be saved in creation status file.
113+ """
114 if not os.path.exists(repo_path):
115 raise ValueError("Repository %s does not exist." % repo_path)
116 file_path = os.path.join(repo_path, REPOSITORY_CREATING_FILE_NAME)
117 if is_creating:
118- open(file_path, 'a').close()
119+ with open(file_path, 'w+') as fd:
120+ fd.write(json.dumps(metadata or {}))
121 else:
122 try:
123 os.unlink(file_path)
124 except OSError:
125 pass
126+ # Remove the AsyncResult creation object from Celery backend.
127+ result = get_creation_async_result(repo_path)
128+ if result:
129+ result.forget()
130
131
132 def is_repository_available(repo_path):
133@@ -458,6 +481,37 @@ def is_repository_available(repo_path):
134 return not os.path.exists(status_file_path)
135
136
137+def get_creation_async_result(repo_path):
138+ status_file_path = os.path.join(repo_path, REPOSITORY_CREATING_FILE_NAME)
139+ if not os.path.exists(status_file_path):
140+ return None
141+ with open(status_file_path) as fd:
142+ metadata = fd.read()
143+ if not metadata:
144+ return None
145+ task_id = json.loads(metadata).get("task_id")
146+ return app.AsyncResult(task_id)
147+
148+
149+def get_repository_creation_status(repo_path):
150+ """
151+ For repositories that are currently beign created though
152+ `init_and_confirm_repo`, this method returns the current status of the
153+ creation.
154+
155+ :return: None if the repository is not being created; Otherwise, one of
156+ the following values:
157+ RUNNING - The creation is still in progress.
158+ FAILED - The creation attempt failed.
159+ SUCCESS - The creating finished successfully
160+ """
161+ if is_repository_available(repo_path):
162+ return None
163+
164+ result = get_creation_async_result(repo_path)
165+ return result.status if result else None
166+
167+
168 def set_default_branch(repo_path, target):
169 repo = Repository(repo_path)
170 repo.set_head(target)
171diff --git a/turnip/api/views.py b/turnip/api/views.py
172index cda3a00..242599f 100644
173--- a/turnip/api/views.py
174+++ b/turnip/api/views.py
175@@ -96,7 +96,8 @@ class RepoAPI(BaseAPI):
176 raise exc.HTTPNotFound()
177 return {
178 'default_branch': store.get_default_branch(repo_path),
179- 'is_available': store.is_repository_available(repo_path)
180+ 'is_available': store.is_repository_available(repo_path),
181+ 'creation_status': store.get_repository_creation_status(repo_path)
182 }
183
184 def _patch_default_branch(self, repo_path, value):
185diff --git a/turnip/tasks.py b/turnip/tasks.py
186index 1f4131a..33612cc 100644
187--- a/turnip/tasks.py
188+++ b/turnip/tasks.py
189@@ -14,7 +14,8 @@ from celery.utils.log import get_task_logger
190 from turnip.config import config
191
192
193-app = Celery('tasks', broker=config.get('celery_broker'))
194+app = Celery('tasks', broker=config.get('celery_broker'),
195+ backend=config.get('celery_backend'))
196 app.conf.update(imports=('turnip.api.store', ))
197
198 logger = get_task_logger(__name__)

Subscribers

People subscribed via source and target branches