Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ticketea internals

ticketea internals

Talk at PyCON ES 2014 (Spanish)

Avatar for Ticketea Engineering

Ticketea Engineering

November 09, 2014
Tweet

More Decks by Ticketea Engineering

Other Decks in Programming

Transcript

  1. ticketea Al Principio… ✤ Primera web, usando .NET ✤ Reescritura

    en PHP y Yii ✤ Reescritura del frontal en PHP (framework propio)
  2. ticketea Actualmente… ✤ De 4 personas a 12, en un

    año y medio ✤ Sin perfil de sistemas/dba puro ✤ Sistemas en AWS ✤ Hemos llegado a varios países
  3. ticketea Herramienta de informes ✤ Business Intelligence ✤ Exportación de

    informes (PDF/XLSX) ✤ Compatibilidad de sesiones con PHP
  4. ticketea Compatibilidad con lo anterior… class SessionStore(CacheStore): def load(self): pass

    def save(self, must_create=False): pass @property def cache_key(self): pass def decode(self, session_data): pass def encode(self, session_dict): pass
  5. ticketea SessionStore -> CacheStore def load(self): try: session_data = self.decode(

    self._cache.get(self.cache_key, None) ) except Exception: session_data = None if session_data is not None and 'user' in session_data: try: session_data[SESSION_KEY] = session_data['user']['id'] except KeyError: pass return session_data self.create() return {}
  6. ticketea SessionStore -> CacheStore def save(self, must_create=False): if must_create: func

    = self._cache.add else: func = self._cache.set result = func( self.cache_key, self.encode(self._get_session(no_load=must_create)), self.get_expiry_age() ) if must_create and not result: raise CreateError
  7. ticketea SessionStore -> CacheStore @property def cache_key(self): return "memc.sess.key." +

    self._get_or_create_session_key() def decode(self, session_data): if session_data is not None: return PHPUnserialize().session_decode(session_data) return session_data def encode(self, session_dict): return PHPSerialize().session_encode(session_dict)
  8. ticketea Usando el admin sin modelos… # __init__.py from .

    import admin_models if 'django.contrib.admin.models' in sys.modules: del sys.modules['django.contrib.admin.models'] sys.modules['django.contrib.admin.models'] = admin_models from django.contrib.admin.sites import AdminSite AdminSite.check_dependencies = nothing from django.core.management.base import BaseCommand BaseCommand.validate = lambda x, *args, **kwargs: None
  9. ticketea ¿Qué es forseti? ✤ Gestionar grupos de auto-escalado ✤

    Generación de AMIs ✤ Despliegue de nuevo código ✤ Gestión de alarmas
  10. ticketea Arquitectura de sistemas API Load Balancer API API API

    { Frontend Frontend Frontend Frontend Load Balancer DB { Usuario
  11. ticketea Resultado ✤ La planificación fue un desastre: 2x tiempo.

    ✤ Mejor logging y reporting de errores. ✤ Tiempo de respuesta reducido a la mitad. ✤ Cobertura de tests > 85%
  12. ticketea django-configurations settings ├── base.py ├── bundles │ ├── crispy.py

    │ ├── gunicorn.py │ ├── pipeline.py │ ├── raven.py │ ├── statsd.py │ └── tkt.py ├── development.py ├── production.py └── testing.py
  13. ticketea django-configurations class Base(CrispyForms, Settings): DJANGO_APPS = [] BUNDLE_APPS =

    [] TKT_APPS = [] @property def INSTALLED_APPS(self): return self.DJANGO_APPS + self.BUNDLE_APPS + self.TKT_APPS @property def DATABASES(self): return { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'db_name', 'USER': 'db_user', 'PASSWORD': self.DB_PASSWORD, 'HOST': 'localhost', } }
  14. ticketea Creando un endpoint… class User(CRUDEndpoint): def __init__(self, pk): super(User,

    self).__init__(pk) self.add_endpoint(Event) def activate(self, activation_key, email): return self._http_client.request.post( '%s/activate' % self.url, data={ 'activation_key': activation_key, 'email': email } )
  15. ticketea Internals del api-client class RestCRUDEndpoint(object): . . . def

    add_endpoint(self, klass): url_endpoint = klass.__name__.lower() python_name = from_camel_to_snake(url_endpoint) list_name = plural_name(python_name) create_name = 'create_' + python_name klass.endpoint = '%s/%s' % (self.url, url_endpoint) klass._http_client = self._http_client setattr(self, python_name, klass) setattr( self, create_name, self._create_object(url_endpoint) )
  16. ticketea Inyectando el api-client en el request class TicketeaApiMiddleware(object): def

    process_request(self, request): if not hasattr(request, 'user'): raise ImproperlyConfigured('error') if isinstance(request.user, AnonymousUser): self._login_as_guest(request) request.api = create_api_client(...) def process_response(self, request, response): # Check if the token was refreshed return response
  17. ticketea Manejando errores del API class TicketeaApiExceptions(object): def process_exception(self, request,

    exception): if isinstance(exception, ApiException): if exception.code == self.REQUEST_TOKEN_EXPIRED: return self._manage_expired_token(request) elif isinstance(exception, ForbiddenException): raise PermissionDenied elif isinstance(exception, NotFoundException): raise Http404
  18. ticketea Mixins class ApiDispatchMixin(object): def dispatch(self, *args, **kwargs): if hasattr(self,

    'get_api_data'): result = self.get_api_data() if isinstance(result, HttpResponse): return result return super(ApiDispatchMixin, self).dispatch( *args, **kwargs )
  19. ticketea Mixins # FAIL class AccessKwargsMixin(object): def get_context_data(self, **kwargs): kw

    = super(AccessKwargsMixin, self) \ .get_context_data(**kwargs) kw['url_kwargs'] = self.kwargs return kw
  20. ticketea Nuestras vistas genéricas class TktView( LoginRequiredMixin, UserInjectorMixin, RoleRequiredMixin, ApiDispatchMixin,

    View ) class TktTemplateView( LoginRequiredMixin, UserInjectorMixin, RoleRequiredMixin, ApiDispatchMixin, TemplateView )
  21. ticketea Clase base de nuestros FormView class TktFormView(FormView): def post(self,

    request, *args, **kwargs): form_class = self.get_form_class() form = self.get_form(form_class) if form.is_valid(): validate_and_commit = getattr(self, 'validate_and_commit', lambda x: True) validate_result = validate_and_commit(form) if validate_result is None: raise ValueError('validate_and_commit must return something') if isinstance(validate_result, HttpResponse): return validate_result elif validate_result: return self.form_valid(form) else: return self.form_invalid(form) else: messages.error(request, _('there has been an error')) return self.form_invalid(form)
  22. ticketea Clase base de nuestros Form @decorator def handle_api_errors(form_save_method, self,

    *args, **kwargs): try: save_method = form_save_method(self, *args, **kwargs) return save_method if save_method is not None else True except ApiException as e: self.fill_form_errors(e.attrs) return False class TktForm(forms.Form): api_field_names = {} def fill_form_errors(self, errors): for field, errno in errors.items(): real_field_name = self.api_field_names.get(field, field) error_description = API_ERRORS_NAMES.get(errno) self._errors[real_field_name] = self.error_class( [error_description] )
  23. ticketea Ejemplo de formulario class AccountInfoForm(TktForm): email = forms.EmailField() def

    __init__(self, *args, **kwargs): super(AccountInfoForm, self).__init__(*args, **kwargs) self.helper = AccountInfoHelper() @handle_api_errors def save(self, user): user.update(**self.cleaned_data)
  24. ticketea Ejemplo de vista class AccountInfoView(TktFormView): form_class = AccountInfoForm template_name

    = 'users_config/users/profile.html' min_role = 'organizer' def get_api_data(self): self.user = self.request.api.user(self.kwargs['id']) def validate_and_commit(self, form): committed = form.save(self.user) if committed: messages.success(self.request, _('OK')) else: messages.error(self.request, _('KO')) return committed def get_success_url(self): return reverse('profile_url')
  25. ticketea third-party apps ✤ django-crispy-forms ✤ django-braces ✤ django-configurations ✤

    django-pipeline ✤ django-secure ✤ django-debug-toolbar ✤ werkzeug ✤ HTTPretty
  26. ticketea Tarea base class BaseTask(Task): abstract = True ignore_result =

    True max_retries = 5 def retry_time(self, retries): RETRIES = [10, 15, 20, 30] try: return RETRIES[retries] except IndexError: return RETRIES[-1] def retry(self, *args, **kwargs): retries = self.request.retries + 1 max_retries = kwargs.get('max_retries', self.max_retries) if retries > max_retries * 0.3: logger.warning(...) kwargs['countdown'] = self.retry_time(retries - 1) return super(BaseTask, self).retry(*args, **kwargs)
  27. ticketea Tarea base class BaseTask(Task): def on_success(self, *args, **kwargs): statsd.incr('tasks.%s'

    % self.name) def on_failure(self, exc, task_id, args, kwargs, einfo): logger.warning(...) def __call__(self, *args, **kwargs): with transaction.atomic(): try: return super(BaseTask, self).__call__( *args, **kwargs ) except Exception as exc: if isinstance(exc, exceptions.Retry): raise exc else: logger.exception('Unhandled exception') self.retry(exc=exc)
  28. ticketea Objetivos ✤ Pasar a sistemas distribuídos ✤ Permitir el

    funcionamiento degradado del sistema ✤ Poder desarrollar cada proyecto a una velocidad distinta ✤ Poder ajustar la infraestructura usada de cada proyecto ✤ Reducir puntos únicos de fallo