NULL on error flipping bits whilst updating pixels

NULL on error underhood

… Ou como preparar o seu blog para escalar para milhões de visitas diárias, mas receber apenas algumas visitas… Da sua mãe, da sua tia, da sua avó, da prima e também da sua namorada :)

Inspirado no artigo Do things, write about it, resolvi escrever um pouco sobre como funciona o meu blog.

A princípio, optei por utilizar o App Engine por dois motivos: 1 - suporta python; e 2 - a quota gratuita é bem generosa para o meu humilde blog :)

Meu objetivo era criar algo extremamente simples, similar a essas ferramentas que geram arquivos estáticos. Um projeto bem popular e que faz isso é o Jekyll, mas como todo bom hacker, decidi reinventar a roda. Já havia feito outros projetos que rodam sob o AppEngine, inclusive a primeira versão do blog, e todos esses projetos seguiam os models da documentação, usando o webapp2 e a engine de renderização do django. Como o AppEngine possui um sistema de arquivos somente leitura, uma alternativa é salvar os dados no BigTable, o que não é um grande problema, uma vez que esses dados ficam no memcached na maior parte do tempo.

Porém, eu queria fazer algo diferente, queria estudar outros frameworks, e foi então que decidi usar o bottle e o jinja2 para renderização de templates. O resultado foi que aprendi muito, inclusive cheguei a escrever alguns filtros para o jinja2: um deles é o imgur, responsável por subir imagens no imgur quando encontra a tag imgur e depois troca pela tag img do html; e o outro, ao encontrar a tag code, invocava o pygments, que por sua vez fazia parser do código e o coloria, mas acabei descartando este filtro e deixando essa tarefa com o cliente, usando o prettify.

Como faço uso de uma quantidade considerável de javascript, pesquisei por algumas soluções de lazy loading, e a que mais me agradou foi o RequireJS. Vamos ver como ficou essa carga logo abaixo:

var require = {
  baseUrl: "/static/js",

  shim: {
    'jquery': {
      exports: '$'
    },

    'bootstrap': {
      deps: ['jquery']
    },

    'prettify': {
      init: function () {
        prettyPrint()
      }
    },

    'jquery.raptorize': {
      deps: ['jquery']
    },

    'clippy': {
      deps: ['jquery'],
      init: function() {
        clippy.BASE_PATH = '/static/agents/'
        clippy.load('Clippy', function(agent) {
          agent.show()
          agent.speak('↑ ↑ ↓ ↓ ← → ← → B A')
          window.agent = agent
          window.setInterval(function() { agent.animate() }, 1500)
        });
      }
    },
  },

  deps: [
    'bootstrap',

    
    {% for module in modules %}
    '{{ module }}',
    {% endfor %}
    
  ],

  callback: function() {
    $(function() {
      
      {% block ready %}
      {% endblock %}
      
    })
  }
}

Adicionar o RequireJS ao projeto é muito simples. Primeiro você configura os módulos que vai usar - quando digo módulos, estou me referindo ao arquivo de javascript, que, no meu caso, se encontram no diretório /static/js, armazenado na váriavel baseUrl.

Outra configuração importante é o shim, este array contém todos os módulos que eventualmente possam ser usados. Você pode indicar se algum módulo depende de outro, como no caso do jQuery.raptorize que depende do jQuery; isso vai garantir que o jQuery.raptorize será carregado depois que o jQuery já tiver sido carregado! Nesta mesma seção, podemos inserir um código na váriavel init que será executado logo após o módulo ser carregado. No caso do clippy temos o seguinte código:

init: function() {
  clippy.BASE_PATH = '/static/agents/'
  clippy.load('Clippy', function(agent) {
    agent.show()
    agent.speak(' ↑ ↑ ↓ ↓ ← → ← → B A')
    window.agent = agent
    window.setInterval(function() { agent.animate() }, 1500)
  });

Ainda neste pequeno bloco de código javascript, temos algumas linhas com tags do jinja2, e um trecho de código que gostaria de destacar são as seguintes linhas:

deps: [
  'bootstrap',

  
  {% for module in modules %}
    '{{ module }}',
  {% endfor %}
  
]

Como podemos ver, pretendo apenas carregar o módulo bootstrap, e como o mesmo depende do jQuery, teremos apenas esses dois módulos carregados por padrão; é aí que entra o jinja2 - esse trecho de código está no arquivo layout.template, que será herdado por todos os outros arquivos de templates. Por padrão, a variável modules vem vazia, mas como podemos ver no template about a seguinte linha:

{% set modules = ['jquery.raptorize', 'clippy'] %}

E é neste momento que atribuo a variável modules com apenas o que vou usar, e não toda aquela tranqueira :)

Bom, isso é um pouco do que acontece no front-end, veremos agora o que se passa por trás das cortinas, no back-end.

Uma das coisas que ficou mais legal, modéstia à parte, foi a forma que o template correto é carregado e renderizado, sem ter que informar o nome do template para a função render, desta forma:

@route('/')
@memorize
def index():
  return render(entries=db.Query(Entry).order('-published').fetch(limit=25))

@route('/entry/:slug')
@memorize
def entry(slug):
  entry = db.Query(Entry).filter('slug =', slug).get()
  if not entry:
    from bottle import HTTPError
    raise HTTPError(404)
  else:
    return render(entry=entry)

@route('/about')
@memorize
def about():
  return render()

No trecho de código acima, temos 3 rotas, que são definidas pelo decorator route:

  • index, que corresponde à url “/”
  • entry que recebe o parâmetro “slug”, que nada mais é do que uma url amigavél
  • about, que não recebe nenhum parâmetro

Nesse momento você deve estar se perguntando: mas como a função render sabe qual template carregar se essa informação não é passada? De uma forma bem simples, meu caro leitor, lembra-se da call stack? Pois então, fazendo uso da biblioteca inspect é possível examinar a call stack em tempo de execução, e assim, saber qual função chamou a função render. Com o nome da função, basta concatenar o nome dela com a extensão template e renderizar com o jinja2 repassando os parâmetros passados

def render(*args, **kwargs):
  import inspect
  callframe = inspect.getouterframes(inspect.currentframe(), 2)
  template = jinja2.get_template('{}.template'.format(callframe[1][3]))
  return template.render(*args, **kwargs)

Isso é que é levar o conceito Don’t Repeat Yourself a sério.

Mais tarde, durante uma entrevista de emprego, vim a saber que alguns frameworks fazem algo parecido, pórem usando exceptions… Bom saber que ainda existem programadores criativos

E por falar em DRY, uma tarefa que acabou ficando extremamente repetitiva foi a manipulação de valores no memcache. Na documentação do AppEngine temos o seguinte exemplo:

def get_data():
  data = memcache.get('key')
  if data is not None:
    return data
  else:
    data = self.query_for_data()
    memcache.add('key', data, 60)
    return data

Contudo, como só pretendo fazer cache das chamadas get, decidi criar um decorator chamado memorize

from google.appengine.api import memcache

class memorize():
  def __init__(self, func):
    self.func = func

  def __call__(self, *args, **kwargs):
    key = '{}/{}'.format(self.func.__name__, '/'.join(kwargs.values()))
    result = memcache.get(key)
    if result is None:
      result = self.func(*args, **kwargs)
      memcache.add(key=key, value=result)
      return result

E adicionar em todas as rotas, como foi visto acima.

Basicamente o que esse decorator faz é gerar uma chave usando a url com seus parâmetros, assim, quando algum parâmetro é alterado, é forçada uma nova chamada à função, e logo após o retorno, é salvo no cache, usando a mesma chave gerada anteriormente. O uso do cache é importante para que o servidor não fique com uma leseira danada.

Para inserir uma nova entrada no blog, deve-se criar dois arquivos, cada um com uma extensão previamente estabelecida (poderia ser apenas um único arquivo e fazer um split usando uma determinada tag, mas achei que ficaria meio chunchado), então são dois arquivos com o mesmo nome, porém um deles é terminado em .entry, que contém o HTML que será exibido, e o outro, terminado em .meta é um YAML com as informações básicas, como título, data de publicação, tags e se é público ou não. Para automatizar essa inserção de dados, faço uso de post-receive hooks do github, ou seja, logo após um git push o github faz uma chamada POST com um json descrito na documentção do github; com esse json em mãos, basta fazer o parsing e ver quais arquivos foram modificados ou adicionados, com as extensões mencionadas acima:

for commit in payload['commits']:
  for action, files  in commit.iteritems():
    if action in ['added', 'modified']:
    for filename in files:
      basename, extension = os.path.splitext(filename)
      if extension in ['.entry', '.meta']:

Assim, basta montar a url completa com a função build_url:

github = {
  'url' : 'https://raw.github.com',
  'repository' : 'nullonerror-posts',
  'user' : 'skhaz',
  'branch' : 'master',
}

def build_url(filename):
  return "%s/%s" % ('/'.join([v for k, v in github.iteritems()]), filename)

Note o “raw” na url, esse é o caminho para o arquivo crú, isso significa que posso baixar o arquivo no seu formato original, assim como foi mencionado acima, tenho dois tipos de arquivos que são tratados de forma especial, como visto abaixo:

from google.appengine.api import urlfetch
from utils import build_url
result = urlfetch.fetch(url = build_url(filename))
if result.status_code == 200:
  entry = Entry.get_or_insert(basename)
if extension.endswith('.entry'):
  entry.content = jinja2.from_string(result.content.decode('utf-8')).render()
else:
  try:
    import yaml
    meta = yaml.load(result.content)
  except:
    logging.error('Failed to parse YAML')
  else:
    entry.title = meta['title']
    entry.categories = meta['categories']
    entry.published = meta['published']
    entry.slug = basename
    entry.put()

Assim como eu posso inserir ou atualizar as entradas, posso fazer o mesmo na hora de remover

elif action in ['removed']:
  for filename in files:
    basename, extension = os.path.splitext(filename)
    entry = Entry.get_by_key_name(basename)
    if entry: entry.delete()

No final do processo, faço um flush no memcached.

Outra coisa bacana no AppEngine, e que não é exclusividade do mesmo, é o PageSpeed. Esse módulo comprime todos os assets, recomprime todas as imagens para formato webp e as serve caso o navegador suporte, codifica em base64 e inclui no html assets muito pequenos para evitar requisições, unifica javascript e css, entre outras técnicas descritas no manual.

Além disso, uso o CloudFlare para CDN, que me ajuda a salvar um bocado de banda :)

Ainda falta implementar muita coisa, estive pensando em escrever um editor em Qt e usar a libgit2 para commitar e fazer push diretamente das alterações.

Enfim… É isso, obrigado por ter lido esse texto longo e chato :)