← Professional Data Engineer: pipelines e decisões de dados
21 / 23 · 135 MIN

Recuperar armazenamento e publicar com condições

Validar destinos de restauro, versões, dependências e publicação transacional com um laboratório SQLite local.

1. Escolher o destino antes de executar o restauro

Recuperar armazenamento começa por uma decisão sobre o destino. Pretendes obter uma cópia para investigar, substituir o conjunto servido à aplicação ou reconstruir uma dependência perdida? Estas intenções têm critérios de aceitação diferentes. Num caso fictício de posições de fundos, uma cópia isolada pode servir para reconciliação sem autorizar a aplicação a mudar de base. Regista a origem do backup, o ponto temporal escolhido, o destino e os consumidores que dependem dele. A existência de um ficheiro ou snapshot não responde a estas perguntas. No Spanner, o restauro de backup cria uma nova base de dados. Não planeies executar o mesmo procedimento sobre uma base existente. Se o destino usa outro projeto ou configuração de instância, prepara primeiro a cópia do backup para um destino compatível. Inclui o tempo dessa cópia na janela, em vez de o descobrir durante o incidente. A nova base também exige capacidade para armazenamento e tráfego. O plano de passagem deve explicar como a aplicação passa a usar o destino, como se mede o resultado e em que condições se mantém a origem atual.

2. Distinguir disponibilidade, otimização e dependências

Uma base restaurada pode estar utilizável antes de atingir o estado operacional pretendido. Em READY_OPTIMIZING, o Spanner permite utilização enquanto continua a copiar e otimizar dados. Uma consulta bem-sucedida não prova a latência que o negócio exige sob carga. A métrica de armazenamento pode ainda não representar o conjunto completo, e a base continua dependente do backup montado. Define um workload de aceitação com volumes e concorrência representativos, em vez de usar apenas uma leitura de uma linha. A recuperação também tem componentes que não regressam automaticamente com os dados. Permissões específicas da base e dados internos de change streams precisam de atenção própria; políticas de eliminação por TTL devem ser reconfiguradas. Distingue permissões herdadas da instância de controlos aplicados diretamente à base. Para backups CMEK, confirma que a chave e a versão necessárias para ler o backup continuam disponíveis. Escolher outra chave para o destino não reconstrói material perdido da origem. O relatório de recuperação deve separar dados reconciliados, aplicação autorizada, continuidade dos consumidores e desempenho aceite. Cada conclusão precisa de evidência correspondente.

3. Recuperar tabelas sem confundir operações

Um table snapshot BigQuery é uma imagem de leitura. Para corrigir dados, cria uma tabela modificável a partir dessa imagem e conserva a evidência que justificou o ponto de recuperação. Criar um snapshot e restaurar uma tabela a partir dele não são a mesma operação: a restrição de não substituir um nome existente na criação não impede que um restauro seja configurado para substituir uma tabela. Antes de autorizar essa substituição, identifica o conteúdo atual que seria perdido e as alternativas de recuperação isolada. Não transportes pressupostos de custo entre mecanismos. Um destino noutra região envolve uma cópia; não deve ser orçamentado como simples metadata sem armazenamento adicional. Mesmo num snapshot que partilha blocos, mudanças na base podem aumentar os blocos cobrados ao snapshot. Reclustering pode reescrever mais dados do que sugere a pequena alteração lógica. Verifica também a expiração de partições aplicada no destino: o snapshot não preserva essa informação como muitos runbooks presumem. A escolha entre snapshot, cópia e tabela modificável deve refletir retenção, custo, isolamento e o trabalho que será permitido sobre os dados recuperados.

4. Proteger versões num data lake

O nome de um objeto é insuficiente para identificar a versão que uma intervenção pretende usar. Entre listar, validar e copiar, outro processo pode ter alterado a origem ou o destino. As pré-condições devem acompanhar a operação: proteger apenas a origem não impede substituir trabalho novo no destino; proteger apenas o destino não impede copiar uma origem entretanto alterada. Para metadata, usa a geração juntamente com a metageneration esperadas. Uma metageneration igual pode existir numa geração diferente do objeto. Em escritas de dados Cloud Storage, a condição de geração zero representa ausência de versão live. Versões não correntes, por si só, não impedem satisfazer esse caso especial. Não interpretes zero como desativar a verificação. Num restauro de soft delete, a nova cópia torna-se live e pode substituir uma versão live que já ocupa o nome; o planeamento tem de considerar essa concorrência. A cópia restaurada fica em Standard, pelo que o custo após recuperação pode diferir do arquivo original. Regista as versões observadas, o conteúdo esperado e a decisão tomada quando uma condição falha, evitando repetir sem compreender o estado atual.

5. Preparar a publicação local com SQLite

O laboratório executa SQL real numa base SQLite em memória. Não escreve na cloud nem abre uma base indicada pelo utilizador. A entrada contém objects, o estado inicial, e batches, os pedidos ordenados. Cada objeto tem key, generation e payload. Cada alteração contém a chave, expectedGeneration, payload e o SHA-256 esperado dos bytes UTF-8. A geração é um contador local, não um identificador Google Cloud. Zero na expectativa significa que a chave deve estar ausente do catálogo local. Cada batch tem um identificador e entre uma e vinte alterações com chaves distintas. O programa valida a estrutura completa antes de iniciar o trabalho. Dentro de BEGIN IMMEDIATE, compara hashes e gerações, escreve as alterações e regista o fingerprint do pedido no ledger. Só depois faz COMMIT. Se um controlo falhar, executa ROLLBACK, incluindo alterações anteriores do mesmo batch. Este contrato transacional pertence à base SQLite do exercício. Não demonstra que um conjunto de objetos Cloud Storage possa ser publicado atomicamente pela mesma sequência de chamadas. Numa plataforma real, os limites de atomicidade e o protocolo de promoção têm de ser desenhados explicitamente.

6. Repetir sem desfazer trabalho posterior

Executa python3 content/labs/pde-atomic-publish/run.py < content/labs/pde-atomic-publish/case.json. O primeiro pedido publica A e B. O segundo chega a alterar A dentro da transação, mas falha porque espera B ausente quando B já existe. O rollback repõe A e o segundo pedido não entra no ledger. A repetição do primeiro pedido responde already-applied. O estado final conserva A na geração 5 e B na geração 1. Lê tanto os resultados dos batches como os objetos e identificadores efetivamente confirmados. Agora introduz uma publicação válida entre o primeiro pedido e a sua repetição. A resposta already-applied não deve voltar a escrever o payload antigo sobre a publicação mais recente. O ledger reconhece a intenção já concluída; não é um comando de rollback. Reutilizar o identificador com conteúdo diferente produz operation-id-conflict. Um identificador novo também não evita uma expectativa de geração antiga. Se um batch falhar, não fica registado como aplicado e uma versão corrigida pode ser tentada depois de nova análise. A persistência deste ledger termina com a execução, pelo que o exercício não promete idempotência através de reinícios. Experimenta inverter a ordem das alterações dentro de um pedido: o fingerprint continua igual porque o programa ordena por chave. Não invertas, porém, a ordem de pedidos dependentes esperando o mesmo resultado. Um pedido que cria A deve anteceder outro que espera essa geração criada. Esta distinção evita confundir repetição de transporte com reordenação de intenções. Para inspecionar o resultado, compara o ledger, a geração e o payload, e não apenas a última mensagem de estado.

7. Voltar a servir o conjunto certo

Recuperar ficheiros não garante que a plataforma consulte o conjunto pretendido. Uma tabela BigLake pode continuar a usar metadata em cache depois de o URI ser alterado para um prefixo recuperado. A passagem precisa de uma atualização explícita da cache e de evidência da consulta resultante. Quando a configuração está em automatic e é necessário atualizar após mudança do URI, segue o procedimento documentado de passar temporariamente a manual, atualizar e repor o modo. Confirma também a localização da execução e o resultado do job de atualização. A proteção criptográfica tem fronteiras semelhantes. A CMEK da tabela e da cache BigQuery não configura automaticamente os ficheiros Cloud Storage subjacentes. No inventário da recuperação, separa dados, metadata, identidades, chaves e referências aos caminhos. A equipa que gere o catálogo pode ser diferente da que gere o bucket, mas o plano deve atribuir a alguém a validação de ponta a ponta. Uma consulta bem-sucedida feita por um administrador não substitui o ensaio com a identidade e as permissões da aplicação. O objetivo é voltar a servir o conjunto aprovado, com o acesso aprovado, e não apenas tornar recursos visíveis.

8. Transformar verificações em critérios de aceitação

No exercício, modifica o hash de B e confirma que A não fica parcialmente publicado. Depois, conserva hashes válidos mas usa taxas de câmbio de uma data errada. O resultado técnico pode ser applied, embora o conjunto seja inadequado para o fecho. Um hash confirma correspondência com bytes declarados, não a verdade da origem, a data correta ou a coerência de negócio. Acrescenta à revisão critérios semânticos: corte temporal, chaves esperadas, reconciliação de totais e relações entre posições e taxas. Num handover, apresenta separadamente o que foi executado, o que foi verificado e o que continua pendente. Por exemplo: base recuperada e saldos reconciliados; aplicação ainda por testar sob carga; consumidores históricos com plano separado; publicação do lake condicionada à atualização da cache. Mantém ligações para os resultados e versões dos artefactos. A regra central é preservar trabalho concorrente e tornar falhas observáveis antes da passagem. Os testes do laboratório demonstram propriedades locais, incluindo rollback e repetição, mas não provam durabilidade, concorrência real entre processos, atomicidade cloud ou autorização de produção. Esses requisitos precisam de ensaio e decisão no ambiente da organização.

"""Original in-memory SQLite publication exercise. No cloud or file writes."""
import hashlib
import json
import re
import sqlite3
import sys


def exact(value, fields):
    if type(value) is not dict or set(value) != set(fields):
        raise ValueError('unexpected or missing fields')


def ident(value):
    if type(value) is not str or not re.fullmatch(r'[A-Za-z0-9_-]{1,48}', value):
        raise ValueError('invalid identifier')


def integer(value, low=0):
    if type(value) is not int or not low <= value <= 10**9:
        raise ValueError('generation outside contract')


def payload(value):
    if type(value) is not str or len(value.encode('utf-8')) > 1000:
        raise ValueError('payload must contain at most 1000 UTF-8 bytes')


def digest(value):
    return hashlib.sha256(value.encode('utf-8')).hexdigest()


def evaluate(document):
    exact(document, ['objects', 'batches'])
    objects, batches = document['objects'], document['batches']
    if type(objects) is not list or len(objects) > 100:
        raise ValueError('at most 100 initial objects')
    if type(batches) is not list or len(batches) > 20:
        raise ValueError('at most 20 ordered batches')
    keys = set()
    for obj in objects:
        exact(obj, ['key', 'generation', 'payload'])
        ident(obj['key']); integer(obj['generation'], 1); payload(obj['payload'])
        if obj['key'] in keys:
            raise ValueError('duplicate initial key')
        keys.add(obj['key'])
    for batch in batches:
        exact(batch, ['id', 'changes']); ident(batch['id'])
        if type(batch['changes']) is not list or not 1 <= len(batch['changes']) <= 20:
            raise ValueError('each batch needs 1 to 20 changes')
        keys = set()
        for change in batch['changes']:
            exact(change, ['key', 'expectedGeneration', 'payload', 'sha256'])
            ident(change['key']); integer(change['expectedGeneration']); payload(change['payload'])
            if type(change['sha256']) is not str or not re.fullmatch(r'[0-9a-f]{64}', change['sha256']):
                raise ValueError('invalid SHA-256 syntax')
            if change['key'] in keys:
                raise ValueError('duplicate key within batch')
            keys.add(change['key'])
    db = sqlite3.connect(':memory:', isolation_level=None)
    results = []
    try:
        db.execute('CREATE TABLE objects (key TEXT PRIMARY KEY, generation INTEGER NOT NULL, payload TEXT NOT NULL)')
        db.execute('CREATE TABLE ledger (id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL)')
        db.executemany('INSERT INTO objects VALUES (?,?,?)', [(x['key'], x['generation'], x['payload']) for x in objects])
        for batch in batches:
            changes = sorted(batch['changes'], key=lambda x: x['key'])
            fingerprint = digest(json.dumps(changes, sort_keys=True, separators=(',', ':'), ensure_ascii=True))
            db.execute('BEGIN IMMEDIATE')
            try:
                previous = db.execute('SELECT fingerprint FROM ledger WHERE id=?', (batch['id'],)).fetchone()
                if previous:
                    status = 'already-applied' if previous[0] == fingerprint else 'operation-id-conflict'
                    db.execute('ROLLBACK')
                    results.append({'id': batch['id'], 'status': status, 'blockedKey': None})
                    continue
                blocked = None
                for change in changes:
                    key = change['key']
                    current = db.execute('SELECT generation FROM objects WHERE key=?', (key,)).fetchone()
                    generation = current[0] if current else 0
                    if digest(change['payload']) != change['sha256']:
                        blocked = ('hash-mismatch', key)
                    elif generation != change['expectedGeneration']:
                        blocked = ('generation-mismatch', key)
                    elif generation == 10**9:
                        blocked = ('generation-limit', key)
                    if blocked:
                        break
                    db.execute('INSERT INTO objects VALUES (?,?,?) ON CONFLICT(key) DO UPDATE SET generation=excluded.generation,payload=excluded.payload', (key, generation + 1, change['payload']))
                if blocked:
                    db.execute('ROLLBACK')
                    results.append({'id': batch['id'], 'status': blocked[0], 'blockedKey': blocked[1]})
                else:
                    db.execute('INSERT INTO ledger VALUES (?,?)', (batch['id'], fingerprint))
                    db.execute('COMMIT')
                    results.append({'id': batch['id'], 'status': 'applied', 'blockedKey': None})
            except Exception:
                if db.in_transaction:
                    db.execute('ROLLBACK')
                raise
        state = [{'key': k, 'generation': g, 'payload': v} for k, g, v in db.execute('SELECT key,generation,payload FROM objects ORDER BY key')]
        ledger = [r[0] for r in db.execute('SELECT id FROM ledger ORDER BY id')]
        return {'objects': state, 'batches': results, 'committedBatchIds': ledger,
                'sqliteVersion': sqlite3.sqlite_version, 'stateLifetime': 'this invocation only',
                'cloudAtomicityProven': False, 'businessCorrectnessProven': False,
                'productionPublicationApproved': False}
    finally:
        db.close()


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError('duplicate JSON field')
        result[key] = value
    return result


if __name__ == '__main__':
    try:
        raw = sys.stdin.read(2_000_001)
        if len(raw) > 2_000_000:
            raise ValueError('input exceeds local limit')
        print(json.dumps(evaluate(json.loads(raw, object_pairs_hook=unique_object)), sort_keys=True))
    except (ValueError, TypeError, RecursionError, UnicodeError, sqlite3.Error) as error:
        print(json.dumps({'error': str(error)}), file=sys.stderr)
        sys.exit(2)
NA PRÁTICA

Caso fictício: posições e taxas recuperadas só podem substituir o conjunto atual se as versões aprovadas ainda coincidirem e a reconciliação de negócio passar.

Armadilhas comuns

Confundir snapshot com restauro; ignorar IAM e consumidores; retirar pré-condições após um conflito; aceitar hashes como prova de dados corretos; inferir atomicidade cloud de uma transação local.

Tópicos relacionados: Pré-condições e concorrência · Snapshots e cópias · Transações e idempotência

Leva esta ideia contigo

Restaurar, reconciliar e publicar são decisões ligadas, cada uma com evidência própria. Preserva versões e trabalho concorrente antes da passagem.

Criar conta

Referência: Professional Data Engineer standard exam guide · Current linked standard guide (document title v4.2); edition date unconfirmed (2026-09-30 inspection)

Google Cloud é uma marca comercial de Google LLC. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por Google. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.