Skip to content

Commit

Permalink
Use HTTP default layer functions instead of Cassette
Browse files Browse the repository at this point in the history
...for a huge speedup.
  • Loading branch information
christopher-dG committed Nov 12, 2020
1 parent 0386587 commit cfb41a9
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 65 deletions.
8 changes: 2 additions & 6 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
name = "BrokenRecord"
uuid = "bdd55f5b-6e67-4da1-a080-6086e55655a0"
authors = ["Chris de Graaf <[email protected]> and contributors"]
version = "0.1.2"
version = "0.1.3"

[deps]
BSON = "fbb218c0-5317-5bc6-957e-2ee96dd4b1f0"
Cassette = "7057c7e9-c182-5462-911a-8362d720325c"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"

[compat]
BSON = "0.2"
Cassette = "0.3"
HTTP = "^0.8.15"
Suppressor = "0.2"
HTTP = "0.9"
julia = "1"

[extras]
Expand Down
48 changes: 31 additions & 17 deletions src/BrokenRecord.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,33 @@ module BrokenRecord

export playback

using Core: kwftype
using Base.Threads: nthreads, threadid

using BSON: bson, load
using Cassette: Cassette, overdub, prehook, @context
using HTTP: HTTP, Header, URI, body_was_streamed, header, mkheaders, nobody, request,
request_uri
using Suppressor: @suppress
using HTTP: HTTP, Header, Layer, Response, URI, body_was_streamed, header, insert_default!,
mkheaders, nobody, remove_default!, request, request_uri, stack, top_layer

const FORMAT = v"1"
const DEFAULTS = Dict(
:path => "",
:ignore_headers => [],
:ignore_query => [],
)

function __init__()
# Hiding the stacktrace from Cassette#174.
ctx = RecordingCtx(; metadata=(; responses=[]))
@suppress try overdub(ctx, () -> HTTP.get("https://httpbin.org/get")) catch end
const STATE = map(1:nthreads()) do i
(; responses=Response[], ignore_headers=String[], ignore_query=String[])
end

drop_keys(keys) = p -> !(p.first in keys)

get_state() = STATE[threadid()]

function reset_state()
state = get_state()
empty!(state.responses)
empty!(state.ignore_headers)
empty!(state.ignore_query)
end

"""
configure!(; path=nothing, ignore_headers=nothing, ignore_query=nothing)
Expand Down Expand Up @@ -54,17 +58,27 @@ function playback(
f, path;
ignore_headers=DEFAULTS[:ignore_headers], ignore_query=DEFAULTS[:ignore_query],
)
metadata = (; responses=[], ignore_headers=ignore_headers, ignore_query=ignore_query)
reset_state()
state = get_state()
append!(state.ignore_headers, ignore_headers)
append!(state.ignore_query, ignore_query)

path = joinpath(DEFAULTS[:path], path)
ctx = if isfile(path)
data = load(path)
PlaybackCtx(; metadata=(; metadata..., responses=data[:responses]))
before_layer, custom_layer = if isfile(path)
top_layer(stack()), PlaybackLayer
else
RecordingCtx(; metadata=metadata)
Union{}, RecordingLayer
end

insert_default!(before_layer, custom_layer)
before(custom_layer, path)
result = try
f()
finally
remove_default!(before_layer, custom_layer)
after(custom_layer, path)
end

result = overdub(ctx, f)
after(ctx, path)
return result
end

Expand Down
52 changes: 18 additions & 34 deletions src/playback.jl
Original file line number Diff line number Diff line change
@@ -1,50 +1,34 @@
@context PlaybackCtx
abstract type PlaybackLayer{Next <: Layer} <: Layer{Next} end

const HTTPMethod = Union{AbstractString, Symbol}
const NoQuery = Dict{SubString{String}, SubString{String}}

function Cassette.prehook(
ctx::PlaybackCtx, ::kwftype(typeof(request)), kwargs, ::typeof(request),
m::HTTPMethod, u, h=Header[], b=nobody,
)
prehook(
ctx,
request,
m,
request_uri(u, get(kwargs, :query, nothing)),
get(kwargs, :headers, h),
get(kwargs, :body, b),
)
function before(::Type{<:PlaybackLayer}, path)
data = load(path)
state = get_state()
append!(state.responses, data[:responses])
end

function Cassette.prehook(
ctx::PlaybackCtx, ::typeof(request), m::HTTPMethod, u, h=Header[], body=nobody,
)
function HTTP.request(::Type{<:PlaybackLayer}, m, u, h=Header[], b=nobody; kwargs...)
method = string(m)
uri = request_uri(u, nothing)
headers = mkheaders(h)
isempty(ctx.metadata.responses) && error("No responses remaining in the data file")
response = ctx.metadata.responses[1]
uri = request_uri(u, get(kwargs, :query, nothing))
headers = mkheaders(get(kwargs, :headers, h))
body = get(kwargs, :body, b)
state = get_state()
isempty(state) && error("No responses remaining in the data file")
response = popfirst!(state.responses)
request = response.request
check_body(request, body)
check_method(request, method)
check_headers(request, headers; ignore=ctx.metadata.ignore_headers)
check_uri(request, uri; ignore=ctx.metadata.ignore_query)
check_headers(request, headers; ignore=state.ignore_headers)
check_uri(request, uri; ignore=state.ignore_query)
return response
end

function Cassette.overdub(
ctx::PlaybackCtx, ::kwftype(typeof(request)), k, ::typeof(request),
m::HTTPMethod, u, h=Header[], b=nobody,
)
return overdub(ctx, request, m, u, h, b)
function after(::Type{<:PlaybackLayer}, path)
state = get_state()
isempty(state.responses) || error("Found unused responses")
end

Cassette.overdub(ctx::PlaybackCtx, ::typeof(request), ::HTTPMethod, u, h, b) =
popfirst!(ctx.metadata.responses)

after(ctx::PlaybackCtx, path) =
isempty(ctx.metadata.responses) || error("Found unused responses")

parse_query(uri) = isempty(uri.query) ? NoQuery() : Dict(split.(split(uri.query, '&'), '='))

function check_body(request, body)
Expand Down
22 changes: 14 additions & 8 deletions src/recording.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
@context RecordingCtx
abstract type RecordingLayer{Next <: Layer} <: Layer{Next} end

Cassette.posthook(ctx::RecordingCtx, resp, ::typeof(request), ::Type{Union{}}, args...) =
push!(ctx.metadata.responses, deepcopy(resp))
before(::Type{<:RecordingLayer}, path) = nothing

function after(ctx::RecordingCtx, path)
for resp in ctx.metadata.responses
filter!(drop_keys(ctx.metadata.ignore_headers), resp.request.headers)
resp.request.target = filter_query(resp.request, ctx.metadata.ignore_query)
function HTTP.request(::Type{RecordingLayer{Next}}, resp) where Next
state = get_state()
push!(state.responses, deepcopy(resp))
return request(Next, resp)
end

function after(::Type{<:RecordingLayer}, path)
state = get_state()
for resp in state.responses
filter!(drop_keys(state.ignore_headers), resp.request.headers)
resp.request.target = filter_query(resp.request, state.ignore_query)
end
mkpath(dirname(path))
bson(path; responses=ctx.metadata.responses, format=FORMAT)
bson(path; responses=state.responses, format=FORMAT)
end

function filter_query(request, ignore)
Expand Down

0 comments on commit cfb41a9

Please sign in to comment.