Skip to content

Commit

Permalink
make protobuf metadata access thread safe
Browse files Browse the repository at this point in the history
Most of the book-keeping data for a protobuf struct is kept inside the struct instance. So that does not hinder thread safe usage. However struct instances themselves need to be locked if they are being read and written to from different threads, as is expected of any regular Julia struct.

Protobuf metadata for a struct (the information about fields and their properties as mentioned in the protobuf IDL definition) however is best initialized once and reused. It was not possible to generate code in such a way that it could be initialized when code is loaded and pre-compiled. This was because of the need to support nested and recursive struct references that protobuf allows - metadata for a struct could be defined only after the struct and all of its dependencies were defined. Metadata initialization had to be deferred to the first constructor call. But in order to reuse the metadata definition, it gets stored into a `Ref` that is set once. A process wide lock is used to make access to it thread safe. There is a small cost to be borne for that, and it should be negligible for most usages.

If an application wishes to eliminate that cost entirely, then the way to do it would be to call the constructors of all protobuf structs it wishes to use first and then switch the lock off by calling `ProtoBuf.enable_async_safety(false)`. Once all metadata definitiions have been initialized, this would allow them to be used without any further locking overhead. This can also be set to `false` for a single threaded synchronous applicaation where it is known that no parallelism is possible.
  • Loading branch information
tanmaykm committed Oct 21, 2020
1 parent d2855ec commit bfb2697
Show file tree
Hide file tree
Showing 17 changed files with 532 additions and 360 deletions.
10 changes: 10 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,22 @@ true
````

## Equality & Hash Value

It is possible for fields marked as optional to be in an "unset" state. Even bits type fields (`isbitstype(T) == true`) can be in this state though they may have valid contents. Such fields should then not be compared for equality or used for computing hash values. All ProtoBuf compatible types, by virtue of extending abstract `ProtoType` type, override `hash`, `isequal` and `==` methods to handle this.

## Other Methods

- `copy!{T}(to::T, from::T)` : shallow copy of objects
- `isfilled(obj)` : same as `isinitialized`
- `lookup(en, val::Integer)` : lookup the name (symbol) corresponding to an enum value
- `enumstr(enumname, enumvalue::Int32)`: returns a string with the enum field name matching the value
- `which_oneof(obj, oneof::Symbol)`: returns a symbol indicating the name of the field in the `oneof` group that is filled

## Thread safety

Most of the book-keeping data for a protobuf struct is kept inside the struct instance. So that does not hinder thread safe usage. However struct instances themselves need to be locked if they are being read and written to from different threads, as is expected of any regular Julia struct.

Protobuf metadata for a struct (the information about fields and their properties as mentioned in the protobuf IDL definition) however is best initialized once and reused. It was not possible to generate code in such a way that it could be initialized when code is loaded and pre-compiled. This was because of the need to support nested and recursive struct references that protobuf allows - metadata for a struct could be defined only after the struct and all of its dependencies were defined. Metadata initialization had to be deferred to the first constructor call. But in order to reuse the metadata definition, it gets stored into a `Ref` that is set once. A process wide lock is used to make access to it thread safe. There is a small cost to be borne for that, and it should be negligible for most usages.

If an application wishes to eliminate that cost entirely, then the way to do it would be to call the constructors of all protobuf structs it wishes to use first and then switch the lock off by calling `ProtoBuf.enable_async_safety(false)`. Once all metadata definitiions have been initialized, this would allow them to be used without any further locking overhead. This can also be set to `false` for a single threaded synchronous application where it is known that no parallelism is possible.

28 changes: 28 additions & 0 deletions src/codec.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
mutable struct ProtoMetaLock
lck::Union{Nothing,ReentrantLock}
end

function Base.lock(f, l::ProtoMetaLock)
(l.lck === nothing) && (return f())
lock(l.lck) do
f()
end
end

const MetaLock = ProtoMetaLock(ReentrantLock())

function enable_async_safety(dolock::Bool)
if dolock
(MetaLock.lck === nothing) && (MetaLock.lck = ReentrantLock())
else
(MetaLock.lck !== nothing) && (MetaLock.lck = nothing)
end
MetaLock.lck
end

const MSB = 0x80
const MASK7 = 0x7f
const MASK8 = 0xff
Expand Down Expand Up @@ -541,6 +563,12 @@ const DEF_ONEOFS = Int[]
const DEF_ONEOF_NAMES = Symbol[]
const DEF_FIELD_TYPES = Dict{Symbol,String}()

function metalock(f)
lock(MetaLock) do
f()
end
end

_resolve_type(relativeto::Type, typ::Type) = typ
_resolve_type(relativeto::Type, typ::String) = Core.eval(relativeto.name.module, Meta.parse(typ))

Expand Down
64 changes: 33 additions & 31 deletions src/gen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -464,43 +464,44 @@ function generate_msgtype(outio::IO, errio::IO, dtype::DescriptorProto, scope::S
end

# generate struct body
println(io, """mutable struct $(dtypename) <: ProtoType
__protobuf_jl_internal_meta::ProtoMeta
__protobuf_jl_internal_values::Dict{Symbol,Any}
function $(dtypename)(; kwargs...)
obj = new(meta($(dtypename)), Dict{Symbol,Any}())
values = obj.__protobuf_jl_internal_values
symdict = obj.__protobuf_jl_internal_meta.symdict
for nv in kwargs
fldname, fldval = nv
fldtype = symdict[fldname].jtyp
(fldname in keys(symdict)) || error(string(typeof(obj), " has no field with name ", fldname))
values[fldname] = isa(fldval, fldtype) ? fldval : convert(fldtype, fldval)
end
obj
end""")
println(io, """
mutable struct $(dtypename) <: ProtoType
__protobuf_jl_internal_meta::ProtoMeta
__protobuf_jl_internal_values::Dict{Symbol,Any}
function $(dtypename)(; kwargs...)
obj = new(meta($(dtypename)), Dict{Symbol,Any}())
values = obj.__protobuf_jl_internal_values
symdict = obj.__protobuf_jl_internal_meta.symdict
for nv in kwargs
fldname, fldval = nv
fldtype = symdict[fldname].jtyp
(fldname in keys(symdict)) || error(string(typeof(obj), " has no field with name ", fldname))
values[fldname] = isa(fldval, fldtype) ? fldval : convert(fldtype, fldval)
end
obj
end""")
println(io, "end # mutable struct $(dtypename)", ismapentry ? " (mapentry)" : "", deferedmode ? " (has cyclic type dependency)" : "")

# generate the meta for this type
@debug("generating meta", dtypename)
_d_fldnums = [1:length(fldnums);]
println(io, "const __meta_$(dtypename) = Ref{ProtoMeta}()")
println(io, "function meta(::Type{$dtypename})")
println(io, " if !isassigned(__meta_$dtypename)")
println(io, " __meta_$(dtypename)[] = target = ProtoMeta($dtypename)")
!isempty(reqflds) && println(io, " req = Symbol[$(join(reqflds, ','))]")
!isempty(defvals) && println(io, " val = Dict{Symbol,Any}($(join(defvals, ", ")))")
(fldnums != _d_fldnums) && println(io, " fnum = Int[$(join(fldnums, ','))]")
!isempty(packedflds) && println(io, " pack = Symbol[$(join(packedflds, ','))]")
!isempty(wtypes) && println(io, " wtype = Dict($(join(wtypes, ", ")))")
println(io, " allflds = Pair{Symbol,Union{Type,String}}[$(join(allflds, ", "))]")
println(io, """const __meta_$(dtypename) = Ref{ProtoMeta}()
function meta(::Type{$dtypename})
ProtoBuf.metalock() do
if !isassigned(__meta_$dtypename)
__meta_$(dtypename)[] = target = ProtoMeta($dtypename)""")
!isempty(reqflds) && println(io, " req = Symbol[$(join(reqflds, ','))]")
!isempty(defvals) && println(io, " val = Dict{Symbol,Any}($(join(defvals, ", ")))")
(fldnums != _d_fldnums) && println(io, " fnum = Int[$(join(fldnums, ','))]")
!isempty(packedflds) && println(io, " pack = Symbol[$(join(packedflds, ','))]")
!isempty(wtypes) && println(io, " wtype = Dict($(join(wtypes, ", ")))")
println(io, " allflds = Pair{Symbol,Union{Type,String}}[$(join(allflds, ", "))]")
if !isempty(oneofs)
println(io, " oneofs = Int[$(join(oneofs, ','))]")
println(io, " oneof_names = Symbol[$(join(oneof_names, ','))]")
println(io, " oneofs = Int[$(join(oneofs, ','))]")
println(io, " oneof_names = Symbol[$(join(oneof_names, ','))]")
end

print(io, " meta(target, $(dtypename), allflds, ")
print(io, " meta(target, $(dtypename), allflds, ")
print(io, isempty(reqflds) ? "ProtoBuf.DEF_REQ, " : "req, ")
print(io, (fldnums == _d_fldnums) ? "ProtoBuf.DEF_FNUM, " : "fnum, ")
print(io, isempty(defvals) ? "ProtoBuf.DEF_VAL, " : "val, ")
Expand All @@ -509,8 +510,9 @@ function generate_msgtype(outio::IO, errio::IO, dtype::DescriptorProto, scope::S
print(io, isempty(oneofs) ? "ProtoBuf.DEF_ONEOFS, " : "oneofs, ")
print(io, isempty(oneofs) ? "ProtoBuf.DEF_ONEOF_NAMES" : "oneof_names")
println(io, ")")
println(io, " end")
println(io, " __meta_$(dtypename)[]")
println(io, " end")
println(io, " __meta_$(dtypename)[]")
println(io, "end")

# generate new getproperty method
Expand Down
12 changes: 7 additions & 5 deletions src/google/any_pb.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ mutable struct _Any <: ProtoType
end # mutable struct _Any
const __meta__Any = Ref{ProtoMeta}()
function meta(::Type{_Any})
if !isassigned(__meta__Any)
__meta__Any[] = target = ProtoMeta(_Any)
allflds = Pair{Symbol,Union{Type,String}}[:type_url => AbstractString, :value => Array{UInt8,1}]
meta(target, _Any, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES)
ProtoBuf.metalock() do
if !isassigned(__meta__Any)
__meta__Any[] = target = ProtoMeta(_Any)
allflds = Pair{Symbol,Union{Type,String}}[:type_url => AbstractString, :value => Array{UInt8,1}]
meta(target, _Any, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES)
end
__meta__Any[]
end
__meta__Any[]
end
function Base.getproperty(obj::_Any, name::Symbol)
if name === :type_url
Expand Down
36 changes: 21 additions & 15 deletions src/google/api_pb.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ mutable struct Method <: ProtoType
end # mutable struct Method
const __meta_Method = Ref{ProtoMeta}()
function meta(::Type{Method})
if !isassigned(__meta_Method)
__meta_Method[] = target = ProtoMeta(Method)
allflds = Pair{Symbol,Union{Type,String}}[:name => AbstractString, :request_type_url => AbstractString, :request_streaming => Bool, :response_type_url => AbstractString, :response_streaming => Bool, :options => Base.Vector{Option}, :syntax => Int32]
meta(target, Method, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES)
ProtoBuf.metalock() do
if !isassigned(__meta_Method)
__meta_Method[] = target = ProtoMeta(Method)
allflds = Pair{Symbol,Union{Type,String}}[:name => AbstractString, :request_type_url => AbstractString, :request_streaming => Bool, :response_type_url => AbstractString, :response_streaming => Bool, :options => Base.Vector{Option}, :syntax => Int32]
meta(target, Method, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES)
end
__meta_Method[]
end
__meta_Method[]
end
function Base.getproperty(obj::Method, name::Symbol)
if name === :name
Expand Down Expand Up @@ -64,12 +66,14 @@ mutable struct Mixin <: ProtoType
end # mutable struct Mixin
const __meta_Mixin = Ref{ProtoMeta}()
function meta(::Type{Mixin})
if !isassigned(__meta_Mixin)
__meta_Mixin[] = target = ProtoMeta(Mixin)
allflds = Pair{Symbol,Union{Type,String}}[:name => AbstractString, :root => AbstractString]
meta(target, Mixin, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES)
ProtoBuf.metalock() do
if !isassigned(__meta_Mixin)
__meta_Mixin[] = target = ProtoMeta(Mixin)
allflds = Pair{Symbol,Union{Type,String}}[:name => AbstractString, :root => AbstractString]
meta(target, Mixin, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES)
end
__meta_Mixin[]
end
__meta_Mixin[]
end
function Base.getproperty(obj::Mixin, name::Symbol)
if name === :name
Expand Down Expand Up @@ -100,12 +104,14 @@ mutable struct Api <: ProtoType
end # mutable struct Api
const __meta_Api = Ref{ProtoMeta}()
function meta(::Type{Api})
if !isassigned(__meta_Api)
__meta_Api[] = target = ProtoMeta(Api)
allflds = Pair{Symbol,Union{Type,String}}[:name => AbstractString, :methods => Base.Vector{Method}, :options => Base.Vector{Option}, :version => AbstractString, :source_context => SourceContext, :mixins => Base.Vector{Mixin}, :syntax => Int32]
meta(target, Api, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES)
ProtoBuf.metalock() do
if !isassigned(__meta_Api)
__meta_Api[] = target = ProtoMeta(Api)
allflds = Pair{Symbol,Union{Type,String}}[:name => AbstractString, :methods => Base.Vector{Method}, :options => Base.Vector{Option}, :version => AbstractString, :source_context => SourceContext, :mixins => Base.Vector{Mixin}, :syntax => Int32]
meta(target, Api, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES)
end
__meta_Api[]
end
__meta_Api[]
end
function Base.getproperty(obj::Api, name::Symbol)
if name === :name
Expand Down
Loading

0 comments on commit bfb2697

Please sign in to comment.