Skip to content

Commit

Permalink
Use HTTP default layer functions instead of Cassette (#1)
Browse files Browse the repository at this point in the history
...for a huge speedup.
  • Loading branch information
christopher-dG authored Nov 12, 2020
1 parent 0386587 commit f07ca72
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 <me@cdg.dev> 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

2 comments on commit f07ca72

@christopher-dG
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register

Release notes:
No user-facing changes, but performance is now significantly higher due to removing usage of Cassette.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/24534

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.3 -m "<description of version>" f07ca72aba41ec94171184ac186c900a9f02b538
git push origin v0.1.3

Please sign in to comment.