Improve javadoc and exception handling for http3-qpack

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2021-09-14 16:09:53 +10:00 committed by Simone Bordet
parent 7b2b74045d
commit 6a825df16e
5 changed files with 110 additions and 66 deletions

View File

@ -21,6 +21,12 @@ public interface Instruction
{
void encode(ByteBufferPool.Lease lease);
/**
* <p>A handler for instructions issued by an {@link QpackEncoder} or {@link QpackDecoder}.</p>
* <p>Note: an encoder SHOULD NOT write an instruction unless sufficient stream and connection flow control
* credit is available for the entire instruction, otherwise a stream containing a large instruction can become
* deadlocked.</p>
*/
interface Handler
{
void onInstructions(List<Instruction> instructions);

View File

@ -28,15 +28,14 @@ import org.eclipse.jetty.http3.qpack.internal.parser.EncodedFieldSection;
import org.eclipse.jetty.http3.qpack.internal.table.DynamicTable;
import org.eclipse.jetty.http3.qpack.internal.table.Entry;
import org.eclipse.jetty.http3.qpack.internal.table.StaticTable;
import org.eclipse.jetty.http3.qpack.internal.util.EncodingException;
import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerParser;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.component.Dumpable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.http3.qpack.QpackException.QPACK_DECODER_STREAM_ERROR;
import static org.eclipse.jetty.http3.qpack.QpackException.QPACK_DECOMPRESSION_FAILED;
import static org.eclipse.jetty.http3.qpack.QpackException.QPACK_ENCODER_STREAM_ERROR;
public class QpackDecoder implements Dumpable
{
@ -77,7 +76,7 @@ public class QpackDecoder implements Dumpable
{
_context = new QpackContext();
_handler = handler;
_parser = new DecoderInstructionParser(new DecoderAdapter());
_parser = new DecoderInstructionParser(new InstructionHandler());
_maxHeaderSize = maxHeaderSize;
}
@ -150,19 +149,21 @@ public class QpackDecoder implements Dumpable
LOG.debug("Deferred Decoding: streamId={}, encodedFieldSection={}", streamId, encodedFieldSection);
_encodedFieldSections.add(encodedFieldSection);
}
}
catch (Throwable t)
{
notifyInstructionHandler();
notifyMetaDataHandler();
throw t;
}
boolean hadMetaData = !_metaDataNotifications.isEmpty();
notifyInstructionHandler();
notifyMetaDataHandler();
return hadMetaData;
}
catch (QpackException.SessionException e)
{
throw e;
}
catch (Throwable t)
{
throw new QpackException.SessionException(QPACK_ENCODER_STREAM_ERROR, t.getMessage(), t);
}
}
/**
* Parse instructions from the Encoder stream. The Encoder stream carries an unframed sequence of instructions from
@ -179,17 +180,17 @@ public class QpackDecoder implements Dumpable
{
_parser.parse(buffer);
}
}
catch (EncodingException e)
{
// There was an error decoding the instruction.
throw new QpackException.SessionException(QPACK_DECODER_STREAM_ERROR, e.getMessage(), e);
}
finally
{
notifyInstructionHandler();
notifyMetaDataHandler();
}
catch (QpackException.SessionException e)
{
throw e;
}
catch (Throwable t)
{
throw new QpackException.SessionException(QPACK_ENCODER_STREAM_ERROR, t.getMessage(), t);
}
}
private void checkEncodedFieldSections() throws QpackException
@ -302,7 +303,7 @@ public class QpackDecoder implements Dumpable
return String.format("QpackDecoder@%x{%s}", hashCode(), _context);
}
private void notifyInstructionHandler() throws QpackException
private void notifyInstructionHandler()
{
if (!_instructions.isEmpty())
_handler.onInstructions(_instructions);
@ -321,7 +322,7 @@ public class QpackDecoder implements Dumpable
/**
* This delivers notifications from the DecoderInstruction parser directly into the Decoder.
*/
class DecoderAdapter implements DecoderInstructionParser.Handler
class InstructionHandler implements DecoderInstructionParser.Handler
{
@Override
public void onSetDynamicTableCapacity(int capacity)

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.http3.qpack;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.EnumSet;
@ -43,6 +44,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.http3.qpack.QpackException.H3_GENERAL_PROTOCOL_ERROR;
import static org.eclipse.jetty.http3.qpack.QpackException.QPACK_DECODER_STREAM_ERROR;
import static org.eclipse.jetty.http3.qpack.QpackException.QPACK_ENCODER_STREAM_ERROR;
public class QpackEncoder implements Dumpable
@ -98,7 +100,7 @@ public class QpackEncoder implements Dumpable
_handler = handler;
_context = new QpackContext();
_maxBlockedStreams = maxBlockedStreams;
_parser = new EncoderInstructionParser(new EncoderAdapter());
_parser = new EncoderInstructionParser(new InstructionHandler());
}
/**
@ -140,6 +142,10 @@ public class QpackEncoder implements Dumpable
List<EncodableEntry> encodableEntries = new ArrayList<>();
DynamicTable dynamicTable = _context.getDynamicTable();
// We need to remember what fields were referenced for each stream for multiple reasons:
// 1. We can only (potentially) block up to SETTINGS_QPACK_BLOCKED_STREAMS by referencing entries which may not have arrived.
// 2. We need to remember reference counts to each entry which are then acknowledged by the remote decoder, this
// allows us to know when we can evict an entry (when it has no un-acknowledged references).
StreamInfo streamInfo = _streamInfoMap.get(streamId);
if (streamInfo == null)
{
@ -149,11 +155,10 @@ public class QpackEncoder implements Dumpable
StreamInfo.SectionInfo sectionInfo = new StreamInfo.SectionInfo();
streamInfo.add(sectionInfo);
try
{
int requiredInsertCount = 0;
// This will also extract pseudo headers from the metadata.
Http3Fields httpFields = new Http3Fields(metadata);
for (HttpField field : httpFields)
for (HttpField field : new Http3Fields(metadata))
{
EncodableEntry entry = encode(streamInfo, field);
encodableEntries.add(entry);
@ -171,6 +176,7 @@ public class QpackEncoder implements Dumpable
int deltaBase = signBit ? requiredInsertCount - base - 1 : base - requiredInsertCount;
// Encode all the entries into the buffer.
int pos = BufferUtil.flipToFill(buffer);
// Encode the Field Section Prefix into the ByteBuffer.
NBitIntegerEncoder.encode(buffer, 8, encodedInsertCount);
@ -183,8 +189,24 @@ public class QpackEncoder implements Dumpable
entry.encode(buffer, base);
}
BufferUtil.flipToFlush(buffer, pos);
notifyInstructionHandler();
}
catch (BufferOverflowException e)
{
// TODO: We have already added to the dynamic table so we need to send the instructions to maintain correct state.
// Can we prevent adding to the table until we know the buffer has enough space?
notifyInstructionHandler();
streamInfo.remove(sectionInfo);
sectionInfo.release();
throw new QpackException.StreamException(H3_GENERAL_PROTOCOL_ERROR, "buffer_space_exceeded", e);
}
catch (Throwable t)
{
// We are failing the whole Session so don't need to send instructions back.
throw new QpackException.SessionException(H3_GENERAL_PROTOCOL_ERROR, "compression_error", t);
}
}
/**
* Parse instructions from the Decoder stream. The Decoder stream carries an unframed sequence of instructions from
@ -194,6 +216,8 @@ public class QpackEncoder implements Dumpable
* @throws QpackException if there was an error parsing or handling the instructions.
*/
public void parseInstructionBuffer(ByteBuffer buffer) throws QpackException
{
try
{
while (BufferUtil.hasContent(buffer))
{
@ -201,6 +225,15 @@ public class QpackEncoder implements Dumpable
}
notifyInstructionHandler();
}
catch (QpackException.SessionException e)
{
throw e;
}
catch (Throwable t)
{
throw new QpackException.SessionException(QPACK_DECODER_STREAM_ERROR, t.getMessage(), t);
}
}
/**
* A speculative insert of a Header into the Encoders Dynamic Table. This will also generate
@ -211,15 +244,15 @@ public class QpackEncoder implements Dumpable
public boolean insert(HttpField field)
{
DynamicTable dynamicTable = _context.getDynamicTable();
if (field.getValue() == null)
field = new HttpField(field.getHeader(), field.getName(), "");
// If we should not index this entry or there is no room to insert it, then just return false.
boolean canCreateEntry = shouldIndex(field) && dynamicTable.canInsert(field);
if (!canCreateEntry)
return false;
// We can always reference on insertion as it will always arrive before any eviction.
// Can we insert by duplicating an existing entry?
Entry entry = _context.get(field);
if (entry != null)
{
@ -230,6 +263,7 @@ public class QpackEncoder implements Dumpable
return true;
}
// Can we insert by referencing a name?
boolean huffman = shouldHuffmanEncode(field);
Entry nameEntry = _context.get(field.getName());
if (nameEntry != null)
@ -241,9 +275,9 @@ public class QpackEncoder implements Dumpable
return true;
}
// Add the entry without referencing an existing entry.
dynamicTable.add(new Entry(field));
_instructions.add(new LiteralNameEntryInstruction(field, huffman));
notifyInstructionHandler();
return true;
}
@ -265,8 +299,6 @@ public class QpackEncoder implements Dumpable
if (field.getValue() == null)
field = new HttpField(field.getHeader(), field.getName(), "");
// TODO: The field.getHeader() could be null.
if (field instanceof PreEncodedHttpField)
return EncodableEntry.getPreEncodedEntry((PreEncodedHttpField)field);
@ -431,7 +463,7 @@ public class QpackEncoder implements Dumpable
_instructions.clear();
}
public class EncoderAdapter implements EncoderInstructionParser.Handler
class InstructionHandler implements EncoderInstructionParser.Handler
{
@Override
public void onSectionAcknowledgement(long streamId) throws QpackException

View File

@ -41,6 +41,11 @@ public class StreamInfo implements Iterable<StreamInfo.SectionInfo>
_sectionInfos.add(sectionInfo);
}
public void remove(SectionInfo sectionInfo)
{
_sectionInfos.remove(sectionInfo);
}
public SectionInfo getCurrentSectionInfo()
{
return _sectionInfos.peek();

View File

@ -187,7 +187,7 @@ public class EncodedFieldSection
public interface EncodedField
{
HttpField decode(QpackContext context) throws QpackException;
HttpField decode(QpackContext context);
}
private static class LiteralField implements EncodedField
@ -222,7 +222,7 @@ public class EncodedFieldSection
}
@Override
public HttpField decode(QpackContext context) throws QpackException
public HttpField decode(QpackContext context)
{
if (_dynamicTable)
return context.getDynamicTable().getAbsolute(_base - (_index + 1)).getHttpField();
@ -241,7 +241,7 @@ public class EncodedFieldSection
}
@Override
public HttpField decode(QpackContext context) throws QpackException
public HttpField decode(QpackContext context)
{
return context.getDynamicTable().getAbsolute(_base + _index).getHttpField();
}
@ -264,7 +264,7 @@ public class EncodedFieldSection
}
@Override
public HttpField decode(QpackContext context) throws QpackException
public HttpField decode(QpackContext context)
{
HttpField field;
if (_dynamicTable)
@ -290,7 +290,7 @@ public class EncodedFieldSection
}
@Override
public HttpField decode(QpackContext context) throws QpackException
public HttpField decode(QpackContext context)
{
HttpField field = context.getDynamicTable().getAbsolute(_base + _nameIndex).getHttpField();
return new HttpField(field.getHeader(), field.getName(), _value);