Skip to content

Commit f76469b

Browse files
mirzakaracicreugnkhaf
authored
Merge changes for release 8.4.0 (#530)
* CLIENT-3744 Check server version before sending user-agent-set command (#515) * CLIENT-3023 v8 Allow repeat background query with the same statement (#513) * CLIENT-3435 Update Go Client to version 1.24 to support FIPS (#510) * Update Go Client to version 1.24 to support FIPS * Bumped to 1.25.1 * CI build changes * Moved mod-vendor * Revert module go version to 1.23 (#517) * Eliminate dynamic configuration watcher goroutine leak (#518) * Eliminate dynamic configuration watcher goroutine leak * remove renamed file * Revert NewYamlConfigProviderWithPath removal * CLIENT-3021 Statement.BinNames must be empty for QueryExecute (#511) * CLIENT-3021 Statement.BinNames must be empty for QueryExecute * CLIENT-3546 Added changes to fall in line with logging messages by other clients (#500) * Added changes to fall in line with logging messages by other clients * Policy cleanup based on QA comments * Added labels and updated log level * Fixes and bug fixes encountered during testing * Fixed test cases and simplified locking logic for dynamic config --------- Co-authored-by: Khosrow Afroozeh <[email protected]> * CLIENT-3120 - Replace an existing node in the cluster when a new peer has the same node name, but a different IP address (#504) * Replace an existing node in the cluster when a new peer has the same node name, but a different IP address * Added tlsName to equality check * Fixed outstanding issue and added ability to use servisealternate * Adding logic to make sure we don't add the same node to removeList multiple times * PR review changes * CLIENT-2418 Skip orphan seeds that do not have peers when other seeds have peers (#514) * CLIENT-2418 Skip orphan seeds that do not have peers when other seeds have peers * CLIENT-3796 - Background query for all records applies only partially (#519) * Fixed atomic.Map Clone bug * Added findNodesToRemove.. (#524) * CLIENT-3810 nil pointer panic occurs on errToAerospikeErr (#523) * CLIENT-3810 nil pointer panic occurs on errToAerospikeErr * Setting connection to nil after close * Add check to mark connection only as salvageable if it is not nil on timeout * Removed redundant conn to nil (#526) * Update CHANGELOG --------- Co-authored-by: Eugene R. <[email protected]> Co-authored-by: Khosrow Afroozeh <[email protected]>
1 parent 7088af0 commit f76469b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1931
-561
lines changed

.github/workflows/build.yml

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,52 +5,77 @@ name: Aerospike Go Client Tests
55

66
env:
77
AEROSPIKE_HOSTS: "127.0.0.1:3000"
8+
GODEBUG: fips140=only
9+
GOFIPS140: latest
810
jobs:
911
build:
1012
runs-on: ubuntu-latest
1113
strategy:
1214
matrix:
1315
go-version:
14-
- 1.23
16+
- 1.25.1
1517
server-version:
16-
- 8.1.0.0-rc4_1
18+
- 8.1.0.0
1719
steps:
18-
- uses: actions/checkout@v3
20+
- uses: actions/checkout@v4
1921
- name: "Setup Go ${{ matrix.go-version }}"
20-
uses: actions/setup-go@v3
22+
uses: actions/setup-go@v4
2123
with:
2224
go-version: "${{ matrix.go-version }}"
2325
cache: true
26+
- name: Fetch dependencies
27+
env:
28+
GODEBUG: fips140=on
29+
run: |
30+
# Install all dependencies
31+
go mod download
32+
33+
# Install ginkgo CLI and gocovmerge for testing
34+
go install github.com/onsi/ginkgo/v2/[email protected]
35+
go mod download github.com/wadey/gocovmerge
36+
37+
# Vendor the dependencies
38+
go mod vendor
39+
2440
- name: Display Go version
2541
run: go version
2642
- name: Set up Aerospike Database
2743
uses: reugn/github-action-aerospike@v1
2844
with:
2945
server-version: ${{ matrix.server-version }}
3046
- name: Test Lua Code
31-
run: go run github.com/onsi/ginkgo/v2/ginkgo -cover -race -r -keep-going -succinct -randomize-suites internal/lua
47+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
48+
run: go run -mod=vendor github.com/onsi/ginkgo/v2/ginkgo -cover -race -r -keep-going -succinct -randomize-suites internal/lua
3249
- name: Test types package
33-
run: go run github.com/onsi/ginkgo/v2/ginkgo -cover -race -r -keep-going -succinct -randomize-suites types
50+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
51+
run: go run -mod=vendor github.com/onsi/ginkgo/v2/ginkgo -cover -race -r -keep-going -succinct -randomize-suites types
3452
- name: Test pkg tests
35-
run: go run github.com/onsi/ginkgo/v2/ginkgo -cover -race -r -keep-going -succinct -randomize-suites pkg
53+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
54+
run: go run -mod=vendor github.com/onsi/ginkgo/v2/ginkgo -cover -race -r -keep-going -succinct -randomize-suites pkg
3655
- name: Build Benchmark tool
37-
run: cd tools/benchmark | go build -tags as_proxy -o benchmark .
56+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
57+
run: cd tools/benchmark | go build -mod=vendor -tags as_proxy -o benchmark .
3858
- name: Build asinfo tool
39-
run: cd tools/asinfo | go build -o asinfo .
59+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
60+
run: cd tools/asinfo | go build -mod=vendor -o asinfo .
4061
- name: Build cli tool
41-
run: cd tools/cli | go build -o cli .
62+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
63+
run: cd tools/cli | go build -mod=vendor -o cli .
4264
- name: Build example files
65+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
4366
run: find examples -name "*.go" -type f -print0 | xargs -0 -n1 go build
4467
- name: Build with Reflection code removed
45-
run: go run github.com/onsi/ginkgo/v2/ginkgo build -tags="as_performance" .
68+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
69+
run: go run -mod=vendor github.com/onsi/ginkgo/v2/ginkgo build -tags="as_performance" .
4670
- name: Build for Google App Engine (unsafe package removed)
47-
run: go run github.com/onsi/ginkgo/v2/ginkgo build -tags="app_engine" .
71+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
72+
run: go run -mod=vendor github.com/onsi/ginkgo/v2/ginkgo build -tags="app_engine" .
4873
- name: Run the tests
49-
run: go run github.com/onsi/ginkgo/v2/ginkgo -coverprofile=./cover_native.out -covermode=atomic -coverpkg=./... -race -keep-going -succinct -randomize-suites -skip="HyperLogLog"
50-
- name: Download gocovmerge
51-
run: go mod download github.com/wadey/gocovmerge
74+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
75+
run: go run -mod=vendor github.com/onsi/ginkgo/v2/ginkgo -coverprofile=./cover_native.out -covermode=atomic -coverpkg=./... -race -keep-going -succinct -randomize-suites -skip="HyperLogLog"
5276
- name: Combine Cover Profiles
53-
run: go run github.com/wadey/gocovmerge cover_*.out > cover_all.out
77+
env: {GOPROXY: off, GOSUMDB: off, GOFLAGS: -mod=vendor}
78+
run: go run -mod=vendor github.com/wadey/gocovmerge cover_*.out > cover_all.out
5479
- name: Check Code Coverage
5580
uses: vladopajic/go-test-coverage@v2
5681
with:

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Change History
22

3+
## October 20 2025: v8.4.0
4+
**Improvements**
5+
- [CLIENT-3435] Update Go client to support FIPS.
6+
7+
**Fixes**
8+
- [CLIENT-3821] Fixed issue where `Network Error from EOF` could occur after server migrations.
9+
- [CLIENT-3810] Fixed issue where nil pointer panic occurs on errToAerospikeErr.
10+
- [CLIENT-3796] Fixed issue where background queries for all records applied only partially.
11+
- [CLIENT-3744] Added server version check before sending the user-agent-set command.
12+
- [CLIENT-2418] Skipped orphan seeds without peers when other seeds have peers.
13+
- [CLIENT-3120] Replaced existing cluster node when a new peer shares the same node name but a different IP address.
14+
- [CLIENT-3546] Resolved inconsistencies between clients when reflecting configuration changes.
15+
- [CLIENT-3023] Fixed inability to repeat background queries using the same statement.
16+
- [CLIENT-3021] Ensured Statement.BinNames is empty for QueryExecute operations.
17+
318
## August 29 2025: v8.3.0
419

520
- **Improvements**

batch_delete_policy.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package aerospike
1616

17+
import "github.com/aerospike/aerospike-client-go/v8/logger"
18+
1719
// BatchDeletePolicy is used in batch delete commands.
1820
type BatchDeletePolicy struct {
1921
// FilterExpression is optional expression filter. If FilterExpression exists and evaluates to false, the specific batch key
@@ -79,7 +81,7 @@ func (bdp *BatchDeletePolicy) toWritePolicy(bp *BatchPolicy, dynConfig *DynConfi
7981
}
8082

8183
config := dynConfig.config
82-
if config != nil && config.Dynamic.BatchDelete != nil {
84+
if config != nil && config.Dynamic != nil && config.Dynamic.BatchDelete != nil {
8385
if config.Dynamic.BatchDelete.DurableDelete != nil {
8486
wp.DurableDelete = *config.Dynamic.BatchDelete.DurableDelete
8587
}
@@ -123,16 +125,25 @@ func (bdp *BatchDeletePolicy) patchDynamic(dynConfig *DynConfig) *BatchDeletePol
123125
}
124126

125127
func (bdp *BatchDeletePolicy) mapDynamic(dynConfig *DynConfig) *BatchDeletePolicy {
126-
if dynConfig.config == nil || dynConfig.config.Dynamic == nil {
128+
config := dynConfig.config
129+
if config == nil || config.Dynamic == nil {
127130
return bdp
128131
}
129132

130-
if dynConfig.config.Dynamic.BatchDelete != nil {
131-
if dynConfig.config.Dynamic.BatchDelete.DurableDelete != nil {
132-
bdp.DurableDelete = *dynConfig.config.Dynamic.BatchDelete.DurableDelete
133+
if config.Dynamic.BatchDelete != nil {
134+
if config.Dynamic.BatchDelete.DurableDelete != nil {
135+
configValue := *config.Dynamic.BatchDelete.DurableDelete
136+
bdp.DurableDelete = configValue
137+
if dynConfig.logUpdate.Load() {
138+
logger.Logger.Info("DurableDelete set to %t", configValue)
139+
}
133140
}
134-
if dynConfig.config.Dynamic.BatchDelete.SendKey != nil {
135-
bdp.SendKey = *dynConfig.config.Dynamic.BatchDelete.SendKey
141+
if config.Dynamic.BatchDelete.SendKey != nil {
142+
configValue := *config.Dynamic.BatchDelete.SendKey
143+
bdp.SendKey = configValue
144+
if dynConfig.logUpdate.Load() {
145+
logger.Logger.Info("SendKey set to %t", configValue)
146+
}
136147
}
137148
}
138149

batch_delete_policy_config_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
package aerospike
1616

1717
import (
18+
"sync/atomic"
19+
1820
dynconfig "github.com/aerospike/aerospike-client-go/v8/config"
1921
gg "github.com/onsi/ginkgo/v2"
2022
gm "github.com/onsi/gomega"
@@ -26,6 +28,8 @@ var _ = gg.Describe("ApplyConfigToBatchDeletePolicy", func() {
2628
gg.It("should update the policy values based on the dynamic config", func() {
2729
// Create the full configuration.
2830
config := &DynConfig{
31+
configInitialized: func() *atomic.Bool { v := &atomic.Bool{}; v.Store(true); return v }(),
32+
logUpdate: func() *atomic.Bool { v := &atomic.Bool{}; v.Store(false); return v }(),
2933
config: &dynconfig.Config{
3034
Dynamic: &dynconfig.DynamicConfig{
3135
BatchDelete: &dynconfig.BatchDelete{
@@ -64,6 +68,8 @@ var _ = gg.Describe("ApplyConfigToBatchDeletePolicy", func() {
6468
gg.It("should update the write policy values based on the batch delete dynamic config", func() {
6569
// Create the full configuration.
6670
config := &DynConfig{
71+
configInitialized: func() *atomic.Bool { v := &atomic.Bool{}; v.Store(true); return v }(),
72+
logUpdate: func() *atomic.Bool { v := &atomic.Bool{}; v.Store(false); return v }(),
6773
config: &dynconfig.Config{
6874
Dynamic: &dynconfig.DynamicConfig{
6975
BatchDelete: &dynconfig.BatchDelete{

batch_policy.go

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package aerospike
1616

1717
import (
1818
"time"
19+
20+
"github.com/aerospike/aerospike-client-go/v8/logger"
1921
)
2022

2123
// BatchPolicy encapsulates parameters for policy attributes used in write operations.
@@ -160,37 +162,75 @@ func (p *BatchPolicy) patchDynamic(dynConfig *DynConfig) *BatchPolicy {
160162
}
161163

162164
func (p *BatchPolicy) mapDynamic(dynConfig *DynConfig) *BatchPolicy {
163-
if dynConfig.config == nil || dynConfig.config.Dynamic == nil {
165+
// Atomically load config to avoid race conditions
166+
currentConfig := dynConfig.config
167+
if currentConfig == nil || currentConfig.Dynamic == nil {
164168
return p
165169
}
166170

167-
if dynConfig.config.Dynamic.BatchRead != nil {
168-
if dynConfig.config.Dynamic.BatchRead.ReadModeAp != nil {
169-
p.ReadModeAP = mapReadModeAPToReadModeAP(*dynConfig.config.Dynamic.BatchRead.ReadModeAp)
171+
if currentConfig.Dynamic.BatchRead != nil {
172+
if currentConfig.Dynamic.BatchRead.ReadModeAp != nil {
173+
configValue := mapReadModeAPToReadModeAP(*currentConfig.Dynamic.BatchRead.ReadModeAp)
174+
p.ReadModeAP = configValue
175+
if dynConfig.logUpdate.Load() {
176+
logger.Logger.Debug("ReadModeAP set to %s", configValue.String())
177+
}
170178
}
171-
if dynConfig.config.Dynamic.BatchRead.ReadModeSc != nil {
172-
p.ReadModeSC = mapReadModeSCToReadModeSC(*dynConfig.config.Dynamic.BatchRead.ReadModeSc)
179+
if currentConfig.Dynamic.BatchRead.ReadModeSc != nil {
180+
configValue := mapReadModeSCToReadModeSC(*currentConfig.Dynamic.BatchRead.ReadModeSc)
181+
p.ReadModeSC = configValue
182+
if dynConfig.logUpdate.Load() {
183+
logger.Logger.Debug("ReadModeSC set to %s", configValue.String())
184+
}
173185
}
174-
if dynConfig.config.Dynamic.BatchRead.TotalTimeout != nil {
175-
p.TotalTimeout = time.Duration(*dynConfig.config.Dynamic.BatchRead.TotalTimeout) * time.Millisecond
186+
if currentConfig.Dynamic.BatchRead.TotalTimeout != nil {
187+
configValue := time.Duration(*currentConfig.Dynamic.BatchRead.TotalTimeout) * time.Millisecond
188+
p.TotalTimeout = configValue
189+
if dynConfig.logUpdate.Load() {
190+
logger.Logger.Debug("TotalTimeout set to %s", configValue.String())
191+
}
176192
}
177-
if dynConfig.config.Dynamic.BatchRead.SocketTimeout != nil {
178-
p.SocketTimeout = time.Duration(*dynConfig.config.Dynamic.BatchRead.SocketTimeout) * time.Millisecond
193+
if currentConfig.Dynamic.BatchRead.SocketTimeout != nil {
194+
configValue := time.Duration(*currentConfig.Dynamic.BatchRead.SocketTimeout) * time.Millisecond
195+
p.SocketTimeout = configValue
196+
if dynConfig.logUpdate.Load() {
197+
logger.Logger.Debug("SocketTimeout set to %s", configValue.String())
198+
}
179199
}
180-
if dynConfig.config.Dynamic.BatchRead.MaxRetries != nil {
181-
p.MaxRetries = *dynConfig.config.Dynamic.BatchRead.MaxRetries
200+
if currentConfig.Dynamic.BatchRead.MaxRetries != nil {
201+
configValue := *currentConfig.Dynamic.BatchRead.MaxRetries
202+
p.MaxRetries = configValue
203+
if dynConfig.logUpdate.Load() {
204+
logger.Logger.Debug("MaxRetries set to %d", configValue)
205+
}
182206
}
183-
if dynConfig.config.Dynamic.BatchRead.SleepBetweenRetries != nil {
184-
p.SleepBetweenRetries = time.Duration(*dynConfig.config.Dynamic.BatchRead.SleepBetweenRetries) * time.Millisecond
207+
if currentConfig.Dynamic.BatchRead.SleepBetweenRetries != nil {
208+
configValue := time.Duration(*currentConfig.Dynamic.BatchRead.SleepBetweenRetries) * time.Millisecond
209+
p.SleepBetweenRetries = configValue
210+
if dynConfig.logUpdate.Load() {
211+
logger.Logger.Debug("SleepBetweenRetries set to %s", configValue.String())
212+
}
185213
}
186-
if dynConfig.config.Dynamic.BatchRead.AllowInline != nil {
187-
p.AllowInline = *dynConfig.config.Dynamic.BatchRead.AllowInline
214+
if currentConfig.Dynamic.BatchRead.AllowInline != nil {
215+
configValue := *currentConfig.Dynamic.BatchRead.AllowInline
216+
p.AllowInline = configValue
217+
if dynConfig.logUpdate.Load() {
218+
logger.Logger.Debug("AllowInline set to %t", configValue)
219+
}
188220
}
189-
if dynConfig.config.Dynamic.BatchRead.AllowInlineSSD != nil {
190-
p.AllowInlineSSD = *dynConfig.config.Dynamic.BatchRead.AllowInlineSSD
221+
if currentConfig.Dynamic.BatchRead.AllowInlineSSD != nil {
222+
configValue := *currentConfig.Dynamic.BatchRead.AllowInlineSSD
223+
p.AllowInlineSSD = configValue
224+
if dynConfig.logUpdate.Load() {
225+
logger.Logger.Debug("AllowInlineSSD set to %t", configValue)
226+
}
191227
}
192-
if dynConfig.config.Dynamic.BatchRead.RespondAllKeys != nil {
193-
p.RespondAllKeys = *dynConfig.config.Dynamic.BatchRead.RespondAllKeys
228+
if currentConfig.Dynamic.BatchRead.RespondAllKeys != nil {
229+
configValue := *currentConfig.Dynamic.BatchRead.RespondAllKeys
230+
p.RespondAllKeys = configValue
231+
if dynConfig.logUpdate.Load() {
232+
logger.Logger.Debug("RespondAllKeys set to %t", configValue)
233+
}
194234
}
195235
}
196236

batch_read_policy.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package aerospike
1616

17+
import "github.com/aerospike/aerospike-client-go/v8/logger"
18+
1719
// BatchReadPolicy attributes used in batch read commands.
1820
type BatchReadPolicy struct {
1921
// FilterExpression is the optional expression filter. If FilterExpression exists and evaluates to false, the specific batch key
@@ -106,16 +108,26 @@ func (brp *BatchReadPolicy) patchDynamic(dynConfig *DynConfig) *BatchReadPolicy
106108
}
107109

108110
func (brp *BatchReadPolicy) mapDynamic(dynConfig *DynConfig) *BatchReadPolicy {
109-
if dynConfig.config == nil || dynConfig.config.Dynamic == nil {
111+
// Atomically load config to avoid race conditions
112+
currentConfig := dynConfig.config
113+
if currentConfig == nil || currentConfig.Dynamic == nil {
110114
return brp
111115
}
112116

113-
if dynConfig.config.Dynamic.BatchRead != nil {
114-
if dynConfig.config.Dynamic.BatchRead.ReadModeAp != nil {
115-
brp.ReadModeAP = mapReadModeAPToReadModeAP(*dynConfig.config.Dynamic.BatchRead.ReadModeAp)
117+
if currentConfig.Dynamic.BatchRead != nil {
118+
if currentConfig.Dynamic.BatchRead.ReadModeAp != nil {
119+
configValue := mapReadModeAPToReadModeAP(*currentConfig.Dynamic.BatchRead.ReadModeAp)
120+
brp.ReadModeAP = configValue
121+
if dynConfig.logUpdate.Load() {
122+
logger.Logger.Info("ReadModeAP set to %s", configValue.String())
123+
}
116124
}
117-
if dynConfig.config.Dynamic.BatchRead.ReadModeSc != nil {
118-
brp.ReadModeSC = mapReadModeSCToReadModeSC(*dynConfig.config.Dynamic.BatchRead.ReadModeSc)
125+
if currentConfig.Dynamic.BatchRead.ReadModeSc != nil {
126+
configValue := mapReadModeSCToReadModeSC(*currentConfig.Dynamic.BatchRead.ReadModeSc)
127+
brp.ReadModeSC = configValue
128+
if dynConfig.logUpdate.Load() {
129+
logger.Logger.Info("ReadModeSC set to %s", configValue.String())
130+
}
119131
}
120132
}
121133

batch_read_policy_config_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package aerospike
1616

1717
import (
18+
"sync/atomic"
1819
"time"
1920

2021
dynconfig "github.com/aerospike/aerospike-client-go/v8/config"
@@ -28,6 +29,8 @@ var _ = gg.Describe("ApplyConfigToBatchReadPolicy", func() {
2829
gg.It("should update the policy values based on the dynamic config", func() {
2930
// Create the full configuration.
3031
config := &DynConfig{
32+
configInitialized: func() *atomic.Bool { v := &atomic.Bool{}; v.Store(true); return v }(),
33+
logUpdate: func() *atomic.Bool { v := &atomic.Bool{}; v.Store(false); return v }(),
3134
config: &dynconfig.Config{
3235
Dynamic: &dynconfig.DynamicConfig{
3336
BatchRead: &dynconfig.BatchRead{
@@ -86,8 +89,9 @@ var _ = gg.Describe("ApplyConfigToBatchReadPolicy", func() {
8689
gg.Context("when applying batch read config to a write policy", func() {
8790
gg.It("should update the write policy values based on the batch read dynamic config", func() {
8891
// Create the full configuration.
89-
9092
config := &DynConfig{
93+
configInitialized: func() *atomic.Bool { v := &atomic.Bool{}; v.Store(true); return v }(),
94+
logUpdate: func() *atomic.Bool { v := &atomic.Bool{}; v.Store(false); return v }(),
9195
config: &dynconfig.Config{
9296
Dynamic: &dynconfig.DynamicConfig{
9397
BatchRead: &dynconfig.BatchRead{
@@ -125,6 +129,7 @@ var _ = gg.Describe("ApplyConfigToBatchReadPolicy", func() {
125129
}
126130

127131
config.client = &Client{dynConfig: config}
132+
config.client.dynDefaultClientPolicy = &atomic.Pointer[ClientPolicy]{}
128133
config.updateCachedPolicies()
129134

130135
// Create an initial BatchPolicy (used for write operations).

0 commit comments

Comments
 (0)