Repository: prometheus/prometheus
File: tsdb/head.go (Go)

Code before:
		deleted            = map[storage.SeriesRef]struct{}{}
		affected           = map[labels.Label]struct{}{}
		staleSeriesDeleted = 0
		chunksRemoved      = 0
	)

	for _, ref := range refs {
		refShard := int(ref) & (h.series.size - 1)
		h.series.locks[refShard].Lock()

		// Copying getByID here to avoid locking and unlocking twice.
		series := h.series.series[refShard][ref]
		if series == nil {
			h.series.locks[refShard].Unlock()
			continue
		}

		if value.IsStaleNaN(series.lastValue) ||
			(series.lastHistogramValue != nil && value.IsStaleNaN(series.lastHistogramValue.Sum)) ||
			(series.lastFloatHistogramValue != nil && value.IsStaleNaN(series.lastFloatHistogramValue.Sum)) {
			staleSeriesDeleted++
		}

		hash := series.lset.Hash()
		hashShard := int(hash) & (h.series.size - 1)
		if hashShard != refShard {
			h.series.locks[hashShard].Lock()
		}

		chunksRemoved += len(series.mmappedChunks)
		if series.headChunks != nil {
			chunksRemoved += series.headChunks.len()
		}

		deleted[storage.SeriesRef(series.ref)] = struct{}{}
		series.lset.Range(func(l labels.Label) { affected[l] = struct{}{} })
		h.series.hashes[hashShard].del(hash, series.ref)
		delete(h.series.series[refShard], series.ref)

		if hashShard != refShard {
			h.series.locks[hashShard].Unlock()
		}
		h.series.locks[refShard].Unlock()
	}

	h.metrics.seriesRemoved.Add(float64(len(deleted)))
	h.metrics.chunksRemoved.Add(float64(chunksRemoved))
	h.metrics.chunks.Sub(float64(chunksRemoved))
	h.numSeries.Sub(uint64(len(deleted)))
	h.numStaleSeries.Sub(uint64(staleSeriesDeleted))


Review comment (bug):
According to Claude the locks are taken in the wrong order this way:

The PR acquires locks in this order:
```go
h.series.locks[refShard].Lock()      // 1. refShard FIRST
if hashShard != refShard {
    h.series.locks[hashShard].Lock() // 2. hashShard SECOND
}
```

However, the GC code path (via `iterForDeletion` → `check` callback at lines 2282 & 2080-2083) uses the **opposite order**:
```go
s.locks[i].Lock()  // 1. hashShard FIRST (i iterates through hash shards)
// ... in check callback ...
if hashShard != refShard {
    s.locks[refShard].Lock()  // 2. refShard SECOND
}
```


Code after:
		deleted            = map[storage.SeriesRef]struct{}{}
		affected           = map[labels.Label]struct{}{}
		staleSeriesDeleted = 0
		chunksRemoved      = 0
	)

	for _, ref := range refs {
		// Delete the reference from the series map.
		// Copying getByID here to avoid locking and unlocking twice.
		refShard := int(ref) & (h.series.size - 1)
		h.series.locks[refShard].Lock()
		series := h.series.series[refShard][ref]
		if series == nil {
			h.series.locks[refShard].Unlock()
			continue
		}
		delete(h.series.series[refShard], series.ref)
		h.series.locks[refShard].Unlock()

		// Delete the reference from the hash.
		hash := series.lset.Hash()
		hashShard := int(hash) & (h.series.size - 1)
		h.series.locks[hashShard].Lock()
		h.series.hashes[hashShard].del(hash, series.ref)
		h.series.locks[hashShard].Unlock()

		if value.IsStaleNaN(series.lastValue) ||
			(series.lastHistogramValue != nil && value.IsStaleNaN(series.lastHistogramValue.Sum)) ||
			(series.lastFloatHistogramValue != nil && value.IsStaleNaN(series.lastFloatHistogramValue.Sum)) {
			staleSeriesDeleted++
		}

		chunksRemoved += len(series.mmappedChunks)
		if series.headChunks != nil {
			chunksRemoved += series.headChunks.len()
		}

		deleted[storage.SeriesRef(series.ref)] = struct{}{}
		series.lset.Range(func(l labels.Label) { affected[l] = struct{}{} })
	}

	h.metrics.seriesRemoved.Add(float64(len(deleted)))
	h.metrics.chunksRemoved.Add(float64(chunksRemoved))
	h.metrics.chunks.Sub(float64(chunksRemoved))
	h.numSeries.Sub(uint64(len(deleted)))
	h.numStaleSeries.Sub(uint64(staleSeriesDeleted))

	// Remove deleted series IDs from the postings lists.
	h.postings.Delete(deleted, affected)

	// Remove tombstones referring to the deleted series.