Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f1700f2

Browse files
authoredApr 14, 2023
e2e: Add thread-safe getters to app state (celestiaorg#713)
* e2e: Add thread-safe getters to app state It appears as though a race condition has been triggered in the E2E app itself due to directly accessing application state inner variables. Make the application state variables private to remove the temptation to access them directly from outside of the package and to highlight their use, and provide thread-safe getters to access the application state properties. Signed-off-by: Thane Thomson <[email protected]> * e2e: Use serializedState to unmarshal state during rollback Signed-off-by: Thane Thomson <[email protected]> * Remove unnecessary comment Signed-off-by: Thane Thomson <[email protected]> * e2e: Condense thread-safe accessors Refactor thread-safe accessors such that we do not unlock and re-lock multiple times during a single ABCI call. Signed-off-by: Thane Thomson <[email protected]> * e2e: Use custom JSON marshalling/unmarshalling to simplify state serialization Signed-off-by: Thane Thomson <[email protected]> * e2e: Remove unnecessary method Signed-off-by: Thane Thomson <[email protected]> * e2e: Swap Query return value order Signed-off-by: Thane Thomson <[email protected]> --------- Signed-off-by: Thane Thomson <[email protected]>
1 parent b0d64fa commit f1700f2

File tree

3 files changed

+105
-40
lines changed

3 files changed

+105
-40
lines changed
 

‎test/e2e/app/app.go

+11-9
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,13 @@ func NewApplication(cfg *Config) (*Application, error) {
125125
}
126126

127127
// Info implements ABCI.
128-
func (app *Application) Info(_ context.Context, _ *abci.RequestInfo) (*abci.ResponseInfo, error) {
128+
func (app *Application) Info(context.Context, *abci.RequestInfo) (*abci.ResponseInfo, error) {
129+
height, hash := app.state.Info()
129130
return &abci.ResponseInfo{
130131
Version: version.ABCIVersion,
131132
AppVersion: appVersion,
132-
LastBlockHeight: int64(app.state.Height),
133-
LastBlockAppHash: app.state.Hash,
133+
LastBlockHeight: int64(height),
134+
LastBlockAppHash: hash,
134135
}, nil
135136
}
136137

@@ -160,7 +161,7 @@ func (app *Application) InitChain(_ context.Context, req *abci.RequestInitChain)
160161
}
161162
}
162163
resp := &abci.ResponseInitChain{
163-
AppHash: app.state.Hash,
164+
AppHash: app.state.GetHash(),
164165
}
165166
if resp.Validators, err = app.validatorUpdates(0); err != nil {
166167
return nil, err
@@ -261,15 +262,16 @@ func (app *Application) Commit(_ context.Context, _ *abci.RequestCommit) (*abci.
261262

262263
// Query implements ABCI.
263264
func (app *Application) Query(_ context.Context, req *abci.RequestQuery) (*abci.ResponseQuery, error) {
265+
value, height := app.state.Query(string(req.Data))
264266
return &abci.ResponseQuery{
265-
Height: int64(app.state.Height),
267+
Height: int64(height),
266268
Key: req.Data,
267-
Value: []byte(app.state.Get(string(req.Data))),
269+
Value: []byte(value),
268270
}, nil
269271
}
270272

271273
// ListSnapshots implements ABCI.
272-
func (app *Application) ListSnapshots(_ context.Context, _ *abci.RequestListSnapshots) (*abci.ResponseListSnapshots, error) {
274+
func (app *Application) ListSnapshots(context.Context, *abci.RequestListSnapshots) (*abci.ResponseListSnapshots, error) {
273275
snapshots, err := app.snapshots.List()
274276
if err != nil {
275277
panic(err)
@@ -493,7 +495,7 @@ func (app *Application) Rollback() error {
493495
}
494496

495497
func (app *Application) getAppHeight() int64 {
496-
initialHeightStr := app.state.Get(prefixReservedKey + suffixInitialHeight)
498+
initialHeightStr, height := app.state.Query(prefixReservedKey + suffixInitialHeight)
497499
if len(initialHeightStr) == 0 {
498500
panic("initial height not set in database")
499501
}
@@ -502,7 +504,7 @@ func (app *Application) getAppHeight() int64 {
502504
panic(fmt.Errorf("malformed initial height %q in database", initialHeightStr))
503505
}
504506

505-
appHeight := int64(app.state.Height)
507+
appHeight := int64(height)
506508
if appHeight == 0 {
507509
appHeight = initialHeight - 1
508510
}

‎test/e2e/app/snapshots.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type SnapshotStore struct {
3131
// NewSnapshotStore creates a new snapshot store.
3232
func NewSnapshotStore(dir string) (*SnapshotStore, error) {
3333
store := &SnapshotStore{dir: dir}
34-
if err := os.MkdirAll(dir, 0755); err != nil {
34+
if err := os.MkdirAll(dir, 0o755); err != nil {
3535
return nil, err
3636
}
3737
if err := store.loadMetadata(); err != nil {
@@ -84,17 +84,17 @@ func (s *SnapshotStore) saveMetadata() error {
8484
func (s *SnapshotStore) Create(state *State) (abci.Snapshot, error) {
8585
s.Lock()
8686
defer s.Unlock()
87-
bz, err := state.Export()
87+
bz, height, stateHash, err := state.Export()
8888
if err != nil {
8989
return abci.Snapshot{}, err
9090
}
9191
snapshot := abci.Snapshot{
92-
Height: state.Height,
92+
Height: height,
9393
Format: 1,
94-
Hash: hashItems(state.Values, state.Height),
94+
Hash: stateHash,
9595
Chunks: byteChunks(bz),
9696
}
97-
err = os.WriteFile(filepath.Join(s.dir, fmt.Sprintf("%v.json", state.Height)), bz, 0o644) //nolint:gosec
97+
err = os.WriteFile(filepath.Join(s.dir, fmt.Sprintf("%v.json", height)), bz, 0o644) //nolint:gosec
9898
if err != nil {
9999
return abci.Snapshot{}, err
100100
}

‎test/e2e/app/state.go

+89-26
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,21 @@ const (
1717
prevStateFileName = "prev_app_state.json"
1818
)
1919

20-
// State is the application state.
21-
type State struct {
22-
sync.RWMutex
20+
// Intermediate type used exclusively in serialization/deserialization of
21+
// State, such that State need not expose any of its internal values publicly.
22+
type serializedState struct {
2323
Height uint64
2424
Values map[string]string
2525
Hash []byte
26+
}
27+
28+
// State is the application state.
29+
type State struct {
30+
sync.RWMutex
31+
height uint64
32+
values map[string]string
33+
hash []byte
2634

27-
// private fields aren't marshaled to disk.
2835
currentFile string
2936
// app saves current and previous state for rollback functionality
3037
previousFile string
@@ -35,12 +42,12 @@ type State struct {
3542
// NewState creates a new state.
3643
func NewState(dir string, persistInterval uint64) (*State, error) {
3744
state := &State{
38-
Values: make(map[string]string),
45+
values: make(map[string]string),
3946
currentFile: filepath.Join(dir, stateFileName),
4047
previousFile: filepath.Join(dir, prevStateFileName),
4148
persistInterval: persistInterval,
4249
}
43-
state.Hash = hashItems(state.Values, state.Height)
50+
state.hash = hashItems(state.values, state.height)
4451
err := state.load()
4552
switch {
4653
case errors.Is(err, os.ErrNotExist):
@@ -66,8 +73,7 @@ func (s *State) load() error {
6673
return fmt.Errorf("failed to read state from %q: %w", s.currentFile, err)
6774
}
6875
}
69-
err = json.Unmarshal(bz, s)
70-
if err != nil {
76+
if err := json.Unmarshal(bz, s); err != nil {
7177
return fmt.Errorf("invalid state data in %q: %w", s.currentFile, err)
7278
}
7379
return nil
@@ -97,11 +103,39 @@ func (s *State) save() error {
97103
return os.Rename(newFile, s.currentFile)
98104
}
99105

106+
// GetHash provides a thread-safe way of accessing a copy of the current state
107+
// hash.
108+
func (s *State) GetHash() []byte {
109+
s.RLock()
110+
defer s.RUnlock()
111+
hash := make([]byte, len(s.hash))
112+
copy(hash, s.hash)
113+
return hash
114+
}
115+
116+
// Info returns both the height and hash simultaneously, and is used in the
117+
// ABCI Info call.
118+
func (s *State) Info() (uint64, []byte) {
119+
s.RLock()
120+
defer s.RUnlock()
121+
height := s.height
122+
hash := make([]byte, len(s.hash))
123+
copy(hash, s.hash)
124+
return height, hash
125+
}
126+
100127
// Export exports key/value pairs as JSON, used for state sync snapshots.
101-
func (s *State) Export() ([]byte, error) {
128+
// Additionally returns the current height and hash of the state.
129+
func (s *State) Export() ([]byte, uint64, []byte, error) {
102130
s.RLock()
103131
defer s.RUnlock()
104-
return json.Marshal(s.Values)
132+
bz, err := json.Marshal(s.values)
133+
if err != nil {
134+
return nil, 0, nil, err
135+
}
136+
height := s.height
137+
stateHash := hashItems(s.values, height)
138+
return bz, height, stateHash, nil
105139
}
106140

107141
// Import imports key/value pairs from JSON bytes, used for InitChain.AppStateBytes and
@@ -114,71 +148,100 @@ func (s *State) Import(height uint64, jsonBytes []byte) error {
114148
if err != nil {
115149
return fmt.Errorf("failed to decode imported JSON data: %w", err)
116150
}
117-
s.Height = height
118-
s.Values = values
119-
s.Hash = hashItems(values, height)
151+
s.height = height
152+
s.values = values
153+
s.hash = hashItems(values, height)
120154
return s.save()
121155
}
122156

123157
// Get fetches a value. A missing value is returned as an empty string.
124158
func (s *State) Get(key string) string {
125159
s.RLock()
126160
defer s.RUnlock()
127-
return s.Values[key]
161+
return s.values[key]
128162
}
129163

130164
// Set sets a value. Setting an empty value is equivalent to deleting it.
131165
func (s *State) Set(key, value string) {
132166
s.Lock()
133167
defer s.Unlock()
134168
if value == "" {
135-
delete(s.Values, key)
169+
delete(s.values, key)
136170
} else {
137-
s.Values[key] = value
171+
s.values[key] = value
138172
}
139173
}
140174

175+
// Query is used in the ABCI Query call, and provides both the current height
176+
// and the value associated with the given key.
177+
func (s *State) Query(key string) (string, uint64) {
178+
s.RLock()
179+
defer s.RUnlock()
180+
height := s.height
181+
value := s.values[key]
182+
return value, height
183+
}
184+
141185
// Finalize is called after applying a block, updating the height and returning the new app_hash
142186
func (s *State) Finalize() []byte {
143187
s.Lock()
144188
defer s.Unlock()
145189
switch {
146-
case s.Height > 0:
147-
s.Height++
190+
case s.height > 0:
191+
s.height++
148192
case s.initialHeight > 0:
149-
s.Height = s.initialHeight
193+
s.height = s.initialHeight
150194
default:
151-
s.Height = 1
195+
s.height = 1
152196
}
153-
s.Hash = hashItems(s.Values, s.Height)
154-
return s.Hash
197+
s.hash = hashItems(s.values, s.height)
198+
return s.hash
155199
}
156200

157201
// Commit commits the current state.
158202
func (s *State) Commit() (uint64, error) {
159203
s.Lock()
160204
defer s.Unlock()
161-
if s.persistInterval > 0 && s.Height%s.persistInterval == 0 {
205+
if s.persistInterval > 0 && s.height%s.persistInterval == 0 {
162206
err := s.save()
163207
if err != nil {
164208
return 0, err
165209
}
166210
}
167-
return s.Height, nil
211+
return s.height, nil
168212
}
169213

170214
func (s *State) Rollback() error {
171215
bz, err := os.ReadFile(s.previousFile)
172216
if err != nil {
173217
return fmt.Errorf("failed to read state from %q: %w", s.previousFile, err)
174218
}
175-
err = json.Unmarshal(bz, s)
176-
if err != nil {
219+
if err := json.Unmarshal(bz, s); err != nil {
177220
return fmt.Errorf("invalid state data in %q: %w", s.previousFile, err)
178221
}
179222
return nil
180223
}
181224

225+
func (s *State) UnmarshalJSON(b []byte) error {
226+
var ss serializedState
227+
if err := json.Unmarshal(b, &ss); err != nil {
228+
return err
229+
}
230+
s.height = ss.Height
231+
s.values = ss.Values
232+
s.hash = ss.Hash
233+
return nil
234+
}
235+
236+
func (s *State) MarshalJSON() ([]byte, error) {
237+
ss := &serializedState{
238+
Height: s.height,
239+
Values: s.values,
240+
Hash: s.hash,
241+
}
242+
return json.Marshal(ss)
243+
}
244+
182245
// hashItems hashes a set of key/value items.
183246
func hashItems(items map[string]string, height uint64) []byte {
184247
keys := make([]string, 0, len(items))

0 commit comments

Comments
 (0)
Please sign in to comment.