Em 03/09 vocês saíram com três tarefas.
escreve_pagina(n, bytes) e le_pagina(n)NOTES.md em que byte do arquivo o registro foi pararHoje a gente confere esse número, conserta o que estiver torto e sobe o nível da implementação. Abram o código agora.
Página de 4096 bytes, cabeçalho de 16, registro de 8. O registro no slot 0 da página 2 começa em que byte?
página 2 → 2 × 4096 = 8192 // início da página
cabeçalho → 8192 + 16 = 8208 // início do slot 0 slot 0 → 8208 + 0 × 8 = 8208 registro → bytes 8208 a 8215 // 8 bytes, id e matrícula
página 2 (bytes 8192 a 12287) +------------+--------+--------+--------+-----+--------+-----------+ | cabeçalho | slot 0 | slot 1 | slot 2 | ... | slot n | livre | +------------+--------+--------+--------+-----+--------+-----------+ 8192 8208 8216 8224 até 12287 16 B 8 B 8 B 8 B 8 B
deslocamento(pagina, slot) = pagina × TAM_PAGINA
+ TAM_CABECALHO
+ slot × TAM_REGISTRO
(pagina, slot) é o RID. É o que o índice vai guardar no M3Devolve exatamente 4096 bytes da página n, ou falha com erro claro.
Nunca devolve 3000 bytes. Nunca devolve vazio. Nunca devolve um pedaço da página vizinha.
Grava exatamente 4096 bytes na página n, ou falha com erro claro.
Recebeu buffer de outro tamanho, é erro de programação. Falhe alto, e não ajuste em silêncio.
O resto do semestre chama essas duas funções. Implemente com cuidado!
Em Python, com o arquivo já aberto em r+b.
TAM_PAGINA = 4096
def le_pagina(f, n):
f.seek(n * TAM_PAGINA)
return f.read(TAM_PAGINA)
def escreve_pagina(f, n, buf):
f.seek(n * TAM_PAGINA)
f.write(buf)
É um bom lugar para começar, e um lugar ruim para parar.
Antes de seguir, rodem esses dez segundos de teste na implementação que vocês acabaram de ver.
f = open("teste.db", "w+b")
escreve_pagina(f, 2, b"A" * 4096)
escreve_pagina(f, 3, b"B" * 4096)
f.close() # fecha de verdade
f = open("teste.db", "r+b") # reabre: o disco é a prova
p2 = le_pagina(f, 2)
print(len(p2), p2 == b"A" * 4096) # ida e volta
print(le_pagina(f, 3)[:1]) # a vizinha continua intacta
print(os.path.getsize("teste.db") % 4096 == 0)
4096 True
b'B'
True
Deu isso nas três linhas? Então a mínima está de pé. Agora reparem no que esse teste não perguntou.
hexdumpÉ assim que vocês olham o arquivo sem passar pelo próprio código. Um exemplo mínimo, depois de gravar o registro (1, 20260001):
$ hexdump -C -s 8192 -n 32 dados.db
pula 8192 bytes mostra 32
00002000 01 00 08 00 02 00 00 00 00 00 00 00 00 00 00 00
00002010 01 00 00 00 a1 24 35 01 00 00 00 00 00 00 00 00
00002000 é 8192 em hexadecimal: o começo da página 200002010 é 8208: 01 00 00 00 é o id, e a1 24 35 01 é a matrícula, de trás para frente porque a máquina é little-endianprintf '%d\n' 0x2010n já foi alocadabuf tem exatamente 4096 byteswrite, o dado está no discoNenhuma das quatro é verdade. Vamos uma por uma.
Um objeto que guarda o arquivo aberto e o número de páginas. Metade das linhas é validação, e é de propósito.
class Pager:
def __init__(self, caminho):
novo = not os.path.exists(caminho)
self.f = open(caminho, "w+b" if novo else "r+b")
self.f.seek(0, os.SEEK_END)
self.n_paginas = self.f.tell() // TAM_PAGINA
def le(self, n):
self._valida(n)
self.f.seek(n * TAM_PAGINA)
buf = self.f.read(TAM_PAGINA)
if len(buf) != TAM_PAGINA:
raise IOError(f"página {n} truncada: {len(buf)} bytes")
return bytearray(buf)
def escreve(self, n, buf):
self._valida(n)
if len(buf) != TAM_PAGINA:
raise ValueError(f"página com {len(buf)} bytes")
self.f.seek(n * TAM_PAGINA)
self.f.write(buf)
def aloca(self):
n = self.n_paginas
self.f.seek(n * TAM_PAGINA)
self.f.write(bytes(TAM_PAGINA)) # 4096 zeros
self.n_paginas += 1
return n
def sync(self):
self.f.flush()
os.fsync(self.f.fileno())
São 16 bytes. Usem 8 agora e deixem 8 reservados, porque mudar o formato no meio do semestre custa remontar todos os arquivos de teste.
byte 0-1 n_slots ocupados // o que o M1 precisa byte 2-3 tamanho do registro // valida o arquivo na abertura byte 4-7 número desta página // detecta página trocada de lugar byte 8-15 reservado // o LSN do M7 mora aqui
seek foi para o lugar certobyte 0-7 "MINIDB\0" // número mágico byte 8-9 versão do formato // 1 byte 10-13 tamanho da página // 4096 byte 14-17 total de páginas byte 18-21 primeira página com espaço livre
Leem e escrevem em um deslocamento sem mexer na posição do arquivo. Some o seek, some a classe inteira de bugs em que duas chamadas se atrapalham.
os.pread(fd, n, off) em Python, pread(2) em C
Gravem com struct.pack("<II", ...), e não com o que a máquina achar. O arquivo precisa ser lido na máquina do colega.
Bancos de verdade fixam isso no formato, e o seu deve fixar também
Trabalhem a página como buffer mutável em vez de recortar e concatenar bytes. No M2 a mesma página vai ser alterada muitas vezes antes de descer para o disco.
Quatro bytes de crc32 no cabeçalho detectam página escrita pela metade. É como o PostgreSQL percebe corrupção.
escreve(n, b) e depois le(n) == b, para n em 0, 1 e 17hexdump -C dados.db | head, e os bytes estão onde a fórmula disseO teste 3 é o que pega erro de seek. Escrevam ele hoje.
aloca() e contagem de páginas(1, 20260001) em 8 bytes e voltarinsere(registro), que acha espaço, grava e devolve o RIDEntrega do M1 em 17/09, às 23h59. Pull request, testes de aceitação passando, NOTES.md com a decisão mais difícil do módulo, e histórico de commits que mostre o trabalho acontecendo.
E, antes de sair, o exercício desta aula
Antes de sair, um teste que o script de aceitação não faz por vocês.
kill -9 sem fechar o arquivo, e veja o que sobrou no discosync() antes do kill -9, e compareNOTES.md o que mudou entre os dois casos, e por quêEsse parágrafo é o começo do M7. Quem entende isso hoje economiza uma semana em novembro.