reloader.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. /*
  2. * Copyright (c) 2016, Psiphon Inc.
  3. * All rights reserved.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19. package common
  20. import (
  21. "os"
  22. "sync"
  23. )
  24. // IsFileChanged uses os.Stat to check if the name, size, or last mod time of the
  25. // file has changed (which is a heuristic, but sufficiently robust for users of this
  26. // function). Returns nil if file has not changed; otherwise, returns a changed
  27. // os.FileInfo which may be used to check for subsequent changes.
  28. func IsFileChanged(path string, previousFileInfo os.FileInfo) (os.FileInfo, error) {
  29. fileInfo, err := os.Stat(path)
  30. if err != nil {
  31. return nil, ContextError(err)
  32. }
  33. changed := previousFileInfo == nil ||
  34. fileInfo.Name() != previousFileInfo.Name() ||
  35. fileInfo.Size() != previousFileInfo.Size() ||
  36. fileInfo.ModTime() != previousFileInfo.ModTime()
  37. if !changed {
  38. return nil, nil
  39. }
  40. return fileInfo, nil
  41. }
  42. // Reloader represents a read-only, in-memory reloadable data object. For example,
  43. // a JSON data file that is loaded into memory and accessed for read-only lookups;
  44. // and from time to time may be reloaded from the same file, updating the memory
  45. // copy.
  46. type Reloader interface {
  47. // Reload reloads the data object. Reload returns a flag indicating if the
  48. // reloadable target has changed and reloaded or remains unchanged. By
  49. // convention, when reloading fails the Reloader should revert to its previous
  50. // in-memory state.
  51. Reload() (bool, error)
  52. // WillReload indicates if the data object is capable of reloading.
  53. WillReload() bool
  54. // LogDescription returns a description to be used for logging
  55. // events related to the Reloader.
  56. LogDescription() string
  57. }
  58. // ReloadableFile is a file-backed Reloader. This type is intended to be embedded
  59. // in other types that add the actual reloadable data structures.
  60. //
  61. // ReloadableFile has a multi-reader mutex for synchronization. Its Reload() function
  62. // will obtain a write lock before reloading the data structures. Actually reloading
  63. // action is to be provided via the reloadAction callback (for example, read the contents
  64. // of the file and unmarshall the contents into data structures). All read access to
  65. // the data structures should be guarded by RLocks on the ReloadableFile mutex.
  66. //
  67. // reloadAction must ensure that data structures revert to their previous state when
  68. // a reload fails.
  69. //
  70. type ReloadableFile struct {
  71. sync.RWMutex
  72. fileName string
  73. fileInfo os.FileInfo
  74. reloadAction func(string) error
  75. }
  76. // NewReloadableFile initializes a new ReloadableFile
  77. func NewReloadableFile(
  78. fileName string,
  79. reloadAction func(string) error) ReloadableFile {
  80. return ReloadableFile{
  81. fileName: fileName,
  82. reloadAction: reloadAction,
  83. }
  84. }
  85. // WillReload indicates whether the ReloadableFile is capable
  86. // of reloading.
  87. func (reloadable *ReloadableFile) WillReload() bool {
  88. return reloadable.fileName != ""
  89. }
  90. // Reload checks if the underlying file has changed (using IsFileChanged semantics, which
  91. // are heuristics) and, when changed, invokes the reloadAction callback which should
  92. // reload, from the file, the in-memory data structures.
  93. // All data structure readers should be blocked by the ReloadableFile mutex.
  94. func (reloadable *ReloadableFile) Reload() (bool, error) {
  95. if !reloadable.WillReload() {
  96. return false, nil
  97. }
  98. // Check whether the file has changed _before_ blocking readers
  99. reloadable.RLock()
  100. changedFileInfo, err := IsFileChanged(reloadable.fileName, reloadable.fileInfo)
  101. reloadable.RUnlock()
  102. if err != nil {
  103. return false, ContextError(err)
  104. }
  105. if changedFileInfo == nil {
  106. return false, nil
  107. }
  108. // ...now block readers
  109. reloadable.Lock()
  110. defer reloadable.Unlock()
  111. err = reloadable.reloadAction(reloadable.fileName)
  112. if err != nil {
  113. return false, ContextError(err)
  114. }
  115. reloadable.fileInfo = changedFileInfo
  116. return true, nil
  117. }
  118. func (reloadable *ReloadableFile) LogDescription() string {
  119. return reloadable.fileName
  120. }